Here is a math exercise (taken from page 2 - in Russian):
There are 100 visually indistinguishable coins of three types: gold, silver and copper (each type occurs at least once). It is known that gold weighs 3 grams each, silver weighs 2 grams each, copper weighs 1 gram each. How can I determine the type of all coins in no more than 101 weighings on two-plate scales without weights?
(Note: I guess that the exercise is wrong and at most 102 weighings are required. However it doesn't matter)
The solution is as follows:
- Take coins one by one from a list of coins and compare each coin with a previous one
- If the coins have the same weight, then we assign them to one group and continue to weigh further
- If we found a heavier coin cj than the previous one, then go to step 2
- If we found a lighter coin ci than the previous one, then keep weighing coins trying to find a coin cj heavier than ci
- If we found a lighter coin instead, then c0 > ci > cj and we know weights of these coins: 3 > 2 > 1. Go to step 3
- Keep comparing coins
- If we found a heavier coin ck than cj, then ci < cj < ck (and weights are 1 < 2 < 3)
- If we found a lighter coin ck than cj, then compare ci and ck
- If ci < ck, then weights of ci, cj, ck are 1, 3, 2
- If ci > ck, then weights of ci, cj, ck are 2, 3, 1
- If ci = ck, then compare ci + ck with cj
- If ci + ck < cj, then weights of ci, cj, ck are 1, 3, 1 (in this case we don't have a sliver coin, so we will use two copper coins instead on steps 3 and 4)
- If ci + ck > cj, then weights of ci, cj, ck are 2, 3, 2
- If ci + ck = cj, then weights of ci, cj, ck are 1, 2, 1
- Compare rest coins with the silver coin (or two copper coins)
- Lighter coins are copper
- Same coins are silver
- Heavier coins are gold
- If on step 1 we found a lighter coin first instead of a heavier one, then we need to compare first heavy coins with a silver coin to determine their weight (it could be a 102th weighing depending on a coin set)
Here is an example of a coin list:
c0 ci cj ck 3 3 2 2 2 3 3 1 1 2 1 3 |_| |___| |_| i j k
Here is a solution in Isabelle HOL:
datatype coin = GC | SC | CC
datatype comp = LT | EQ | GT
primrec coin_weight :: "coin ⇒ nat" where
"coin_weight CC = 1"
| "coin_weight SC = 2"
| "coin_weight GC = 3"
primrec sum_list where
"sum_list f [] = 0"
| "sum_list f (x # xs) = f x + sum_list f xs"
definition weigh :: "coin list ⇒ coin list ⇒ comp" where
"weigh xs ys = (
let xw = sum_list coin_weight xs in
let yw = sum_list coin_weight ys in
if xw < yw then LT else
if xw > yw then GT else EQ)"
definition std_weigh :: "coin list ⇒ coin ⇒ nat" where
"std_weigh xs ys ≡ (case weigh xs [ys] of LT ⇒ 3 | GT ⇒ 1 | EQ ⇒ 2)"
definition gen_weights :: "coin list ⇒ coin ⇒ coin list ⇒ nat ⇒ nat ⇒ nat ⇒ nat ⇒ nat ⇒ nat ⇒ nat list" where
"gen_weights cs c⇩0 std i j k w⇩j w⇩k w ≡
― ‹Optional heavy coins (\<^term>‹c⇩0›...)›
replicate i (std_weigh std c⇩0) @
― ‹Light coins (\<^term>‹c⇩i›...)›
replicate j w⇩j @
― ‹Heavy coins (\<^term>‹c⇩j›...)›
replicate k w⇩k @
― ‹A light coin (\<^term>‹c⇩k›)›
[w] @
― ‹Rest coins›
map (std_weigh std) cs"
primrec determine_weights where
"determine_weights [] c⇩0 c⇩i c⇩j i j k = None"
| "determine_weights (c⇩k # cs) c⇩0 c⇩i c⇩j i j k = (
case weigh [c⇩j] [c⇩k]
of LT ⇒ Some (gen_weights cs c⇩0 [c⇩j] i j (Suc k) 1 2 3)
| GT ⇒ Some (
case weigh [c⇩i] [c⇩k]
of LT ⇒ gen_weights cs c⇩0 [c⇩k] i j (Suc k) 1 3 2
| GT ⇒ gen_weights cs c⇩0 [c⇩i] i j (Suc k) 2 3 1
| EQ ⇒ (
case weigh [c⇩i, c⇩k] [c⇩j]
of LT ⇒ gen_weights cs c⇩0 [c⇩i, c⇩k] i j (Suc k) 1 3 1
| GT ⇒ gen_weights cs c⇩0 [c⇩k] i j (Suc k) 2 3 2
| EQ ⇒ gen_weights cs c⇩0 [c⇩j] i j (Suc k) 1 2 1))
| EQ ⇒ determine_weights cs c⇩0 c⇩i c⇩j i j (Suc k))"
primrec find_heavier where
"find_heavier [] c⇩0 c⇩i i j alt = None"
| "find_heavier (c⇩j # cs) c⇩0 c⇩i i j alt = (
case weigh [c⇩i] [c⇩j]
of LT ⇒ determine_weights cs c⇩0 c⇩i c⇩j i (Suc j) 0
| GT ⇒ alt cs c⇩j (Suc j)
| EQ ⇒ find_heavier cs c⇩0 c⇩i i (Suc j) alt)"
primrec weigh_coins where
"weigh_coins [] = Some []"
| "weigh_coins (c⇩0 # cs) =
find_heavier cs c⇩0 c⇩0 0 0
(λcs c⇩i i. find_heavier cs c⇩0 c⇩i i 0
(λcs c⇩j j. Some (gen_weights cs c⇩0 [c⇩i] 0 i j 3 2 1)))"
I can prove that the solution is valid for a concrete case:
definition "coins ≡ [GC, GC, SC, SC, SC, GC, GC, CC, CC, SC, CC, GC]"
value "weigh_coins coins"
lemma weigh_coins_ok:
"cs = coins ⟹
weigh_coins cs = Some ws ⟹
ws = map coin_weight cs"
by (induct cs; auto simp: coins_def weigh_def gen_weights_def std_weigh_def)
lemma weigh_coins_length_ok:
"cs = coins ⟹
weigh_coins cs = Some ws ⟹
length cs = length ws"
by (induct cs; auto simp: coins_def weigh_def gen_weights_def std_weigh_def)
However I have no idea how to prove it for a general case:
lemma weigh_coins_ok:
"weigh_coins cs = Some ws ⟹
ws = map coin_weight cs"
proof (induct cs)
case Nil
then show ?case by simp
next
case (Cons c cs)
then show ?case
qed
I can't induct over cs
because I'll need to prove that
weigh_coins (c # cs) = Some ws ⟹ ∃ws. weigh_coins cs = Some ws
It doesn't hold. I can determine weights for [CC, SC, GC]
, but can't do it for [SC, GC]
.
An alternative approach is to prove these lemmas for a special cases:
[CC, CC, ...] @ [SC, SC, ...] @ [GC, GC, ...] @ ...
[CC, CC, ...] @ [GC, GC, ...] @ [SC, SC, ...] @ ...
[SC, SC, ...] @ [GC, GC, ...] @ [CC, CC, ...] @ ...
...
And then to prove that the list of cases is exhaustive.
For example:
lemma weigh_coins_length:
"cs = [CC] @ replicate n CC @ [SC, GC] ⟹
weigh_coins cs = Some ws ⟹
length cs = length ws"
apply (induct n arbitrary: cs ws)
apply (auto simp: weigh_def gen_weights_def std_weigh_def)[1]
However I can't prove even this lemma.
The questions are:
- Could you suggest how such a lemmas can be proven or how to reformulate the functions to make the lemmas provable?
- How to formulate the lemma that
weigh
function is used at mostn + 2
times in the algorithm, wheren
is the number of coins?