Return to Blog

Solving the Fake Coin problem in Python

By John Lekberg on June 20, 2020.


This week's post is about solving the "Fake Coin" problem. You will learn:

Problem statement

There are n identical coins. One coin is fake and weighs less than the real coins. The only way to compare coins is a balance scale.

Your goal is to identify the fake coin.

How I represent the data

When I use the balance scale, the result is either that the scale is balanced, or that one side is heavier. I represent this result using an Enum:

import enum

Scale = enum.Enum(
    "Scale", ["LeftHeavy", "RightHeavy", "Balanced"]
)

I represent the coins using numbers. Since the fake coin weighs less, I use a smaller number for the fake coin:

coin_real = 2
coin_fake = 1

(The specific numbers I chose don't matter, as long as coin_fake < coin_real.)

To create a ranom set of n coins, I have a function that creates a list of coins, then shuffles it:

import random


def random_coins(n):
    """A shuffled list of (n-1) real coins and 1 fake coin."""
    assert n > 0
    coins = [coin_real] * (n - 1) + [coin_fake] * (1)
    random.shuffle(coins)
    return coins

random_coins(10)
[2, 2, 1, 2, 2, 2, 2, 2, 2, 2]

To compare the coins in the balance scale, I have a comparison function that sums the weight of both sides and compares the sums:

def compare(left, right):
    """Weight two collections of coins and return the
    result (a Scale enum).
    """
    delta = sum(right) - sum(left)
    if delta < 0:
        return Scale.LeftHeavy
    elif delta > 0:
        return Scale.RightHeavy
    else:
        return Scale.Balanced

compare([coin_fake], [coin_real])
<Scale.RightHeavy: 2>

Creating a brute force solution

A simple brute force solution will take one coin and compare it to every other coin:

Here's a Python function that implements this:

def find_fake_coin_bf(*, coins, compare):
    """Find the fake coin by using brute force."""
    if len(coins) == 1:
        return coins[0]

    [first, *rest] = coins
    for coin in rest:
        result = compare([first], [coin])
        if result == Scale.LeftHeavy:
            return coin
        elif result == Scale.RightHeavy:
            return first
        elif result == Scale.Balanced:
            pass

    raise RuntimeError("No fake coin found.")

I test this function by generating different sets of random coins and checking that the correct coin is returned:

test_pass = 0
test_fail = 0

for _ in range(100):
    coins = random_coins(20)
    found = find_fake_coin_bf(coins=coins, compare=compare)
    if found == coin_fake:
        test_pass += 1
    else:
        test_fail += 1

print("tests passed:", test_pass)
print("tests failed:", test_fail)
tests passed: 100
tests failed: 0

What's the time complexity of this solution in terms of the number of comparisons? For a collection of n coins,

As a result, the overall time complexity of the function is

O(n) comparisons

Creating a solution that uses less comparisons

Instead of comparing two coins at a time (one in each pan of the balance scale), what if I compared more?

If I have n coins, then I can have two equal piles of n/2⌋ coins (with some coins leftover).

Here's an algorithm that compares as many coins as possible at once. Starting with n coins:

Here's a Python function that implements this algorithm:

def find_fake_coin_fast(*, coins, compare):
    """Find the fake coin quickly by comparing as
    many coins as possible at once.
    """
    pile = list(coins)
    while len(pile) > 1:
        n = len(pile) // 2
        A = pile[:n]
        B = pile[n : 2 * n]

        result = compare(A, B)
        if result == Scale.LeftHeavy:
            pile = B
        elif result == Scale.RightHeavy:
            pile = A
        elif result == Scale.Balanced:
            C = pile[2 * n :]
            pile = C

    return pile[0]

I test this function by generating different sets of random coins and checking that the correct coin is returned:

test_pass = 0
test_fail = 0

for _ in range(100):
    coins = random_coins(20)
    found = find_fake_coin_fast(
        coins=coins, compare=compare
    )
    if found == coin_fake:
        test_pass += 1
    else:
        test_fail += 1

print("tests passed:", test_pass)
print("tests failed:", test_fail)
tests passed: 100
tests failed: 0

What's the time complexity of this solution in terms of the number of comparisons? For a collection of n coins:

As a result, the overall time complexity of the algorithm is

O(log n) comparisons

In conclusion...

In this week's post, you learned how to solve the "Fake Coin" problem using a brute force algorithm. You also learned how to efficiently reduce the pile size by comparing as many coins as possible at once.

My challenge to you:

Modify the algorithm to create piles of size roughly n/3 (instead of n/2). Does this improve the expected number of comparisons to find the fake coin?

If you enjoyed this week's post, share it with your friends and stay tuned for next week's post. See you then!