The Twelve Balls Problem

Recently I caught up with a interesting math problem called the twelve balls problem.

Codes

Following is a piece of code as a simulated solver.

# https://puzzling.stackexchange.com/questions/183/twelve-balls-and-a-scale
def run():
    # setup
    balls_weight = [1] * 12
    fake_idx = np.random.randint(0, 12)
    balls_weight[fake_idx] = np.random.randint(0, 2)+ 0.5
    print(balls_weight)
    
    # functions
    def weigh(idxa, idxb):
        # the simple weighing function
        # idxa, idxb being indexes
        # represent 3 results from scale (to left, equal, to right) of three-state boolean of -1, 0, 1
        left = [balls_weight[a] for a in idxa]
        right = [balls_weight[b] for b in idxb]
        res_func = lambda x: 1 if x > 0 else -1 if x < 0 else 0
        return res_func(np.sum(left) - np.sum(right))

    def three_ball_situation(ia,ib,ic, case_boolean):
        # this function solves a generic 3-ball situation with 1 step
        # ia, ib, ic being indexes
        # case True: either a/b has plus weight or c has minus weight
        # case False: either a/b has minus weight or c has plus weight
        # returns the fake ball index and weighing state: True for plus weight, False for minus weight
        res3 = weigh([ia], [ib])
        if res3 == 0:
            return ic, not case_boolean
        else:
            return ib if case_boolean^(res3 > 0) else ia, case_boolean


    # step 1: weigh balls 0~7
    res1 = weigh([0,1,2,3], [4,5,6,7])
    if res1 == 0:
        # balls 0~7 are good
        # step 2: weigh balls 9,10 to 8 and a good ball (0)
        res2 = weigh([0,8], [9,10])
        if res2 == 0:
            # ball 11 is fake
            # step 3: weigh ball 11 to a good ball (0)
            res3 = weigh([0], [11])
            return 11, True if res3 < 0 else False if res3 > 0 else 'MyMathFailedError'
        else:
            # step 3: three-ball situation with 9/10 in the same direction (based on step 2 result) against 8 
            return three_ball_situation(9,10,8, res2 < 0)
    else:
        # balls 8~11 are good
        # remove good balls and 3/6/7 (still questionable); switch sides of 4 and 2; finally add a good ball (8) to the right
        res2 = weigh([0,1,4], [2,5,8])
        if res2 == 0:
            # 0/1/2/4/5 are good
            # step 3: three-ball situation with 6/7 in the same direction (based on step 1 result) against 3 
            return three_ball_situation(6,7,3,res1 < 0)
        elif res1 == res2:
            # scale status didn't change, balls that didn't switch sides in step 1 and 2 (0/1/5) are questionable
            # step 3: three-ball situation with 0/1 in the same direction (based on step 1/2 result) against 5
            return three_ball_situation(0,1,5,res1 > 0)
        else:
            # scale status changed, balls that switched sides (2/4) are questionable
            # step 3: weigh ball 2 to a good ball (8)
            res3 = weigh([2], [8])
            return 4 if res3 == 0 else 2, (res2 > 0)^(res3 != 0)

Simulations

… and here are some of the simulation results.

for i in range(20):
    idx, res = run()
    print(f'fake pos = {idx+1}, fake is heavier: {res}')
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.5, 1]
fake pos = 11, fake is heavier: True
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.5]
fake pos = 12, fake is heavier: True
[1.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
fake pos = 1, fake is heavier: True
[1, 1, 1, 1, 1, 1, 1.5, 1, 1, 1, 1, 1]
fake pos = 7, fake is heavier: True
[1, 1, 0.5, 1, 1, 1, 1, 1, 1, 1, 1, 1]
fake pos = 3, fake is heavier: False
[1, 1, 1, 1, 1, 1, 1, 1, 1, 0.5, 1, 1]
fake pos = 10, fake is heavier: False
[1, 1, 1, 1, 1, 1.5, 1, 1, 1, 1, 1, 1]
fake pos = 6, fake is heavier: True
[1, 1, 1, 1, 1.5, 1, 1, 1, 1, 1, 1, 1]
fake pos = 5, fake is heavier: True
[1, 0.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
fake pos = 2, fake is heavier: False
[1, 1, 1, 1, 0.5, 1, 1, 1, 1, 1, 1, 1]
fake pos = 5, fake is heavier: False
[0.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
fake pos = 1, fake is heavier: False
[1, 1, 1, 0.5, 1, 1, 1, 1, 1, 1, 1, 1]
fake pos = 4, fake is heavier: False
[1, 1.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
fake pos = 2, fake is heavier: True
[1, 1, 1, 1, 1, 1, 1, 1.5, 1, 1, 1, 1]
fake pos = 8, fake is heavier: True
[1, 1, 1, 1, 1, 1.5, 1, 1, 1, 1, 1, 1]
fake pos = 6, fake is heavier: True
[1, 1, 1.5, 1, 1, 1, 1, 1, 1, 1, 1, 1]
fake pos = 3, fake is heavier: True
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.5, 1]
fake pos = 11, fake is heavier: True
[1, 1.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
fake pos = 2, fake is heavier: True
[1, 1, 0.5, 1, 1, 1, 1, 1, 1, 1, 1, 1]
fake pos = 3, fake is heavier: False
[1, 1, 1, 1, 0.5, 1, 1, 1, 1, 1, 1, 1]
fake pos = 5, fake is heavier: False

Thoughts

The best part of the problem is probably not the solution itself, but how one can guide his way to it.