The 12-Coins is a classic balance puzzle where the goal is find a fake coin from one of 12 using only 3 trials on the balance scale. Furthermore, once you find the fake coin you should be able to determine if it is lighter or heavier than the real coins.
The solution involves dividing the coins into piles that are then weighed against each other. Then using the results of those weighing to find candidate coins as well as identifying known good coins which can then be used in subsequent trials to reduce the number of unknowns.
For example. if you divide the initial set into 3 piles of 4 coins each and weigh 2 of the piles you have only 2 possible results. Either the scale balances because the 2 piles are of equal weight, or the scales will tip because one of the coins is heavier or lighter than the others. If the scale balances then you know all 8 of those coins must be good because they all weigh the same and the fake coin is in the remaining pile. If the scale does not balance then you know all 4 coins in the third pile must be good because the fake one must on the scale causing the piles to be unbalanced.
The package below will solve the puzzle. The twelve coins are lettered A through L. Using the SOLVE procedure you can pick one to be the fake and decide if it will be heavy or light. Alternately you have have the package assign the fake coin and weight difference randomly. A third parameter will determine if the solution will start with a fixed arrangement of coins A,B,C,D in the left pan of the scale and E,F,G,H in the right pan, with I,J,K,L left off the scale – or if the initial distribution of the 3 piles will be randomized. It makes no difference in the final outcome but randomizing may help assure the readers that the algorithm works in the general case and not just for a specific arrangement.
The DO_ALL procedure will iterate through all permutations of coin selection and choice of heavy or light.
It may seem like cheating to give the answer to the package and then ask it to solve the puzzle; but the choices of fake coin and weight difference are not referenced anywhere except in the WEIGH_COINS function which only returns a result of left-heavy, right-heavy, or balanced. The 3 steps of the solving algorithm never reference the answer values.
When weighing the coins, the function will use DBMS_OUTPUT to display a simple ASCII-art representation of the scale and balance result. The following block shows an example of a randomized solution for a heavy F-coin with the 3 weight trials used to solve it.
SQL> BEGIN 2 twelve_coins.solve(p_fake=>'F', p_heavy_fake=> true); 3 END; 4 / -------------------------------- EJGI__-*-__CDKH AB____ \ * \ ____FC A_____-*-_____B Fake coin: F is heavy --------------------------------
You can see step 1 randomly assigned the coins to the pans but the first 8 were balanced. Therefore the fake must be within (A, B, F, L.)
For step 2 two of the unknown coins were put in the left pan (A, B) and one of them in the right along with a known good coin from the initial set (F, C). The result is the right pan is heavier but we don’t know yet if that is because one of (A, B) is light, or if F is heavy. We know C is good from the step 1 balance so those are the only options.
For step 3, the left pan coins were compared to each other. Since they are balanced, they must both be good coins, meaning the fake must have been in the right pan which was heavy. Since the only unknown coin in the right pan was F, we know F is the fake and that it must be heavy since its pan was heavier than the other.
And thus the puzzle was solved in 3 tries.
I found the 12-Coins puzzle a fun mental exercise and enjoyed coming up with a package to implement the solution. I hope you find it interesting and a helpful code example.
CREATE OR REPLACE PACKAGE twelve_coins AS PROCEDURE solve(p_fake IN CHAR DEFAULT NULL, p_heavy_fake IN BOOLEAN DEFAULT NULL, p_random IN BOOLEAN DEFAULT TRUE); PROCEDURE do_all(p_random IN BOOLEAN DEFAULT FALSE); END; /
CREATE OR REPLACE PACKAGE BODY twelve_coins AS c_left_heavy_template CONSTANT VARCHAR2(100) := ' ~RIGHT~ / * ~LEFT~/ '; c_right_heavy_template CONSTANT VARCHAR2(100) := ' ~LEFT~ \ * \ ~RIGHT~ '; c_balanced_template CONSTANT VARCHAR2(100) := ' ~LEFT~-*-~RIGHT~ '; SUBTYPE scale_result IS PLS_INTEGER RANGE -1 .. 1; c_left_heavy CONSTANT scale_result := -1; c_right_heavy CONSTANT scale_result := 1; c_balanced CONSTANT scale_result := 0; TYPE coin_tab IS TABLE OF CHAR(1); c_all_coins CONSTANT coin_tab := coin_tab('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L') ; g_fake CHAR(1); g_heavy_fake BOOLEAN; FUNCTION show_coins(p_coins IN coin_tab) RETURN VARCHAR2 IS -- Construct a text string of the coin pile v_result VARCHAR2(12); BEGIN FOR i IN 1 .. p_coins.COUNT LOOP v_result := v_result || p_coins(i); END LOOP; RETURN v_result; END show_coins; FUNCTION weigh_coins(p_left IN coin_tab, p_right IN coin_tab) RETURN INTEGER IS -- Given the left and right piles, check the result using the heavy pile v_scales VARCHAR2(100); v_balance INTEGER; BEGIN CASE WHEN g_fake MEMBER OF p_left AND g_heavy_fake THEN v_scales := c_left_heavy_template; v_balance := c_left_heavy; WHEN g_fake MEMBER OF p_left AND NOT g_heavy_fake THEN v_scales := c_right_heavy_template; v_balance := c_right_heavy; WHEN g_fake MEMBER OF p_right AND g_heavy_fake THEN v_scales := c_right_heavy_template; v_balance := c_right_heavy; WHEN g_fake MEMBER OF p_right AND NOT g_heavy_fake THEN v_scales := c_left_heavy_template; v_balance := c_left_heavy; ELSE v_scales := c_balanced_template; v_balance := c_balanced; END CASE; -- Show an ASCII "drawing" of the scales v_scales := REPLACE(v_scales, '~LEFT~', RPAD(show_coins(p_left), 6, '_')); v_scales := REPLACE(v_scales, '~RIGHT~', LPAD(show_coins(p_right), 6, '_')); DBMS_OUTPUT.put_line(v_scales); RETURN v_balance; END weigh_coins; PROCEDURE randomize(p_left OUT coin_tab, p_right OUT coin_tab) IS -- distribute the coins randomly across -- the initial left/right piles on the scale BEGIN p_left := coin_tab(NULL, NULL, NULL, NULL); p_right := coin_tab(NULL, NULL, NULL, NULL); FOR x IN (SELECT ROWNUM rn, n FROM ( SELECT LEVEL n FROM DUAL CONNECT BY LEVEL <= 12 ORDER BY DBMS_RANDOM.VALUE) WHERE ROWNUM <= 8) LOOP IF x.rn <= 4 THEN p_left(x.rn) := c_all_coins(x.n); ELSE p_right(x.rn - 4) := c_all_coins(x.n); END IF; END LOOP; END randomize; PROCEDURE step_1(p_heavy OUT coin_tab, p_light OUT coin_tab, p_unknown OUT coin_tab, p_balance OUT scale_result, p_random IN BOOLEAN DEFAULT TRUE) IS v_left coin_tab; v_right coin_tab; BEGIN -- Divide the coins into 3 piles, weigh 2 of them. IF p_random THEN randomize(v_left, v_right); ELSE v_left := coin_tab('A', 'B', 'C', 'D'); v_right := coin_tab('E', 'F', 'G', 'H'); END IF; p_balance := weigh_coins(v_left, v_right); CASE p_balance WHEN c_left_heavy THEN p_heavy := v_left; p_light := v_right; p_unknown := NULL; -- The remaining coins are all good, neither heavy nor light WHEN c_right_heavy THEN p_heavy := v_right; p_light := v_left; p_unknown := NULL; -- The remaining coins are all good, neither heavy nor light ELSE p_heavy := NULL; -- Both pans are equal meaning the fake must be in the 3rd pile p_light := NULL; -- Both pans are equal meaning the fake must be in the 3rd pile p_unknown := c_all_coins MULTISET EXCEPT v_left MULTISET EXCEPT v_right; END CASE; END step_1; PROCEDURE step_2(p_heavy IN coin_tab, p_light IN coin_tab, p_unknowns IN coin_tab, p_balance OUT scale_result) IS v_left coin_tab; v_right coin_tab; v_allgood coin_tab; v_onegood CHAR(1); BEGIN CASE WHEN p_unknowns IS NOT NULL THEN -- If the unknowns are populated, then the fake is among them v_allgood := c_all_coins MULTISET EXCEPT p_unknowns; v_onegood := v_allgood(1); v_left := coin_tab(p_unknowns(1), p_unknowns(2)); v_right := coin_tab(p_unknowns(3), v_onegood); ELSE v_allgood := c_all_coins MULTISET EXCEPT p_heavy MULTISET EXCEPT p_light; v_onegood := v_allgood(1); v_left := coin_tab(p_heavy(1), p_heavy(2), p_light(1)); v_right := coin_tab(p_heavy(3), p_light(2), v_onegood); END CASE; p_balance := weigh_coins(v_left, v_right); END step_2; FUNCTION step_3(p_heavy IN coin_tab, p_light IN coin_tab, p_unknowns IN coin_tab, p_balance_1 IN scale_result, p_balance_2 IN scale_result) RETURN VARCHAR2 IS v_result VARCHAR2(10); v_allgood coin_tab; v_onegood CHAR(1); v_balance scale_result; BEGIN CASE WHEN p_balance_1 = c_balanced THEN IF p_balance_2 = c_balanced THEN v_allgood := c_all_coins MULTISET EXCEPT p_unknowns; v_onegood := v_allgood(1); v_balance := weigh_coins(coin_tab(p_unknowns(4)), coin_tab(v_onegood)); v_result := p_unknowns(4) || CASE v_balance WHEN c_left_heavy THEN ' is heavy' ELSE ' is light' END; ELSE -- balance 2 uneven v_balance := weigh_coins(coin_tab(p_unknowns(1)), coin_tab(p_unknowns(2))); v_result := CASE WHEN v_balance = c_balanced AND p_balance_2 = c_left_heavy THEN p_unknowns(3) || ' is light' WHEN v_balance = c_balanced AND p_balance_2 = c_right_heavy THEN p_unknowns(3) || ' is heavy' WHEN v_balance = p_balance_2 AND v_balance = c_left_heavy THEN p_unknowns(1) || ' is heavy' WHEN v_balance = p_balance_2 AND v_balance = c_right_heavy THEN p_unknowns(1) || ' is light' WHEN v_balance != p_balance_2 AND v_balance = c_left_heavy THEN p_unknowns(2) || ' is light' WHEN v_balance != p_balance_2 AND v_balance = c_right_heavy THEN p_unknowns(2) || ' is heavy' ELSE NULL -- can't happen END; END IF; ELSE -- balance 1 uneven v_allgood := c_all_coins MULTISET EXCEPT p_heavy MULTISET EXCEPT p_light; v_onegood := v_allgood(1); v_result := CASE p_balance_2 WHEN c_balanced THEN CASE weigh_coins(coin_tab(p_light(3)), coin_tab(p_light(4))) WHEN c_balanced THEN p_heavy(4) || ' is heavy' WHEN c_left_heavy THEN p_light(4) || ' is light' ELSE p_light(3) || ' is light' END WHEN c_left_heavy THEN CASE weigh_coins(coin_tab(p_heavy(1)), coin_tab(p_heavy(2))) WHEN c_left_heavy THEN p_heavy(1) || ' is heavy' WHEN c_right_heavy THEN p_heavy(2) || ' is heavy' ELSE p_light(2) || ' is light' END WHEN c_right_heavy THEN CASE weigh_coins(coin_tab(p_heavy(3)), coin_tab(v_onegood)) WHEN c_left_heavy THEN p_heavy(3) || ' is heavy' ELSE p_light(1) || ' is light' END END; END CASE; RETURN v_result; END step_3; PROCEDURE solve(p_fake IN CHAR DEFAULT NULL, p_heavy_fake IN BOOLEAN DEFAULT NULL, p_random IN BOOLEAN DEFAULT TRUE) IS v_heavy coin_tab; v_light coin_tab; v_unknown coin_tab; v_result VARCHAR2(10); v_balance_1 scale_result; v_balance_2 scale_result; BEGIN -- If the user selected a fake, use it, otherwise pick one at random CASE WHEN p_fake IS NULL THEN g_fake := CHR(ROUND(DBMS_RANDOM.VALUE(ASCII('A'), ASCII('L')))); WHEN p_fake NOT MEMBER OF c_all_coins THEN RAISE VALUE_ERROR; ELSE g_fake := p_fake; END CASE; -- If the user chose heavy/light use it, otherwise pick at random IF p_heavy_fake IS NULL THEN g_heavy_fake := DBMS_RANDOM.VALUE < 0.5; ELSE g_heavy_fake := p_heavy_fake; END IF; DBMS_OUTPUT.put_line(RPAD('-', 30, '-')); step_1(v_heavy, v_light, v_unknown, v_balance_1, p_random); step_2(v_heavy, v_light, v_unknown, v_balance_2); v_result := step_3(v_heavy, v_light, v_unknown, v_balance_1, v_balance_2); DBMS_OUTPUT.put_line('Fake coin: ' || v_result); DBMS_OUTPUT.put_line(RPAD('-', 30, '-')); END solve; PROCEDURE do_all(p_random IN BOOLEAN DEFAULT FALSE) IS -- Run trials for each of the 12 coins being the fake -- Run each coin trial twice, once with the fake heavy and again light BEGIN FOR i IN 1 .. 12 LOOP FOR j IN 1 .. 2 LOOP solve(p_fake => c_all_coins(i), p_heavy_fake => CASE j WHEN 1 THEN TRUE ELSE FALSE END, p_random => p_random); END LOOP; END LOOP; END do_all; END; /