Sometimes these puzzles leave a bit too much to the imagination. ]]>

**Solution:** We can use the timers to time 9 minutes with only 9 minutes total time elapsing as follows:

]]>At t=0m start both timers.

At t=4m the 4 minute timer expires. Turn it over to start it again.

At t=7m the 7 minute timer expires. Turn it over to start it again.

At t=8m the 4 minute timer expires again, and the 7 minute timer has been running for 1 minute. Turn the 7 minute timer over.

At t=9m the 7 minute timer expires, and the egg is ready.

It runs in 210ms.

**Run:** [ @repl.it ]

from enigma import irange, nsplit, multiset, compare, Accumulator, tuples, join, nconcat, printf # 4-digit cubes, expressed as a sequence of digits cubes = list(nsplit(i ** 3, 4) for i in irange(10, 21)) # a and b can be linked if they share at least 2 digits def link(a, b, fn=lambda n: n > 1): return fn(len(multiset(a).intersection(b))) # find chains def chains(chain, vs): # is this a valid chain? if link(chain[-1], chain[0]): yield chain # can we extend it? for (i, v) in enumerate(vs): if not(v < chain[0]) and link(chain[-1], v): yield from chains(chain + [v], vs[:i] + vs[i + 1:]) def maxlen(vs, v): r = compare(len(v), len(vs[0])) if r < 0: # shorter return vs elif r > 0: # longer return [v] else: # equal return vs + [v] r = Accumulator(fn=maxlen, value=[[]]) # consider each starting value for (i, v) in enumerate(cubes): r.accumulate_from(chains([v], cubes[:i] + cubes[i + 1:])) # format a chain fmt = lambda ss: join((nconcat(s) for s in ss), sep=" - ") # look at the maximal length chains n = len(r.value[0]) printf("[maxlen = {n}]") for chain in r.value: if n > 2 and chain[1] > chain[-1]: continue xs = chain + [chain[0]] if all(link(a, b, fn=lambda n: n == 2) for (a, b) in tuples(xs, 2)): printf("[2] ({chain})", chain=fmt(xs)) else: printf("[1] ({chain})", chain=fmt(xs)) # find 9261 i = chain.index((9, 2, 6, 1)) printf(" -> ({links})", links=fmt([chain[i - 1], chain[i], xs[i + 1]]))

**Solution:** The neighbours of 9261 are: (1) 2197, 6859; (2) 1728, 4096.

There are 12 possible cubes, but the longest possible chains are of length 10.

There are essentially two different chains of length 10:

[1] (1331 – 3375 – 5832 – 1728 – 2744 – 2197 – 9261 – 6859 – 4096 – 4913 – 1331)

In this chain the (2197 – 9261) link has 3 digits in common (1, 2, 9).

[2] (1331 – 3375 – 5832 – 6859 – 4096 – 9261 – 1728 – 2744 – 2197 – 4913 – 1331)

In this chain all the links have exactly 2 digits in common.

]]>**Run:** [ @repl.it ]

from collections import defaultdict from enigma import subsets, multiset, printf # gifts gifts = ('TS', 'BH', 'BS', 'GS') # questions q1 = lambda x: x == 'TS' # "is tea strainer" q2 = lambda x: x in ('BH', 'BS') # "for the bath" q3 = lambda x: x in ('BH', 'GS') # "something to wear" # answers for each girl (Y, N) = (1, 0) ans = [ (Y, Y, N), # J (Y, N, Y), # K (N, Y, Y), # L (N, N, Y), # M ] # check the girl with gift <g> gives at least 2 correct responses <rs> def correct(g, rs): return sum(q(g) == x for (q, x) in zip((q1, q2, q3), rs)) # record possible solutions ss = list() # choose an assignment for the gifts for gs in subsets(gifts, size=len, select="P"): # check each girl gives at least two true answers if any(correct(g, rs) < 2 for (g, rs) in zip(gs, ans)): continue ss.append(gs) (J, K, L, M) = gs printf("[J={J} K={K} L={L} M={M}]") # for each girl accumulate the number of scenarios for each gift r = defaultdict(multiset) for s in ss: for (i, x) in enumerate(s): r[i].add(x) # look for scenarios where only one girl is has a unique gift for gs in ss: if sum(r[i][g] == 1 for (i, g) in enumerate(gs)) != 1: continue (J, K, L, M) = gs printf("J={J} K={K} L={L} M={M}")

**Solution:** Jane got the bath soap; Kate got the tea-strainer; Lucy got the gloves; Maud got the bath hat.

There are three assignments of gifts to girls where each of the girls gives at least 2 correct responses.

Lucy is always able to deduce what the other girls got knowing her own gift. But in one of the scenarios Maud will also be able to tell, and in another scenario both Jane and Kate can also tell. So the scenario where only Lucy can tell is the one we are interested in.

]]>

J=TS;K=GS;L=BS; M=BH

J=BS; K=TS;L=BH;M=GS

J=BS; K=TS;L=GS; M=BH [SOLUTION]

16T² = (a + b + c)(–a + b + c)(a – b + c)(a + b – c)

= 2(a²b² + a²c² + b²c²) – (a⁴ + b⁴ + c⁴)

= 2(AB + AC + BC) – (A² + B² + C²)

Then:

(A + B + C)² = (A² + B² + C²) + 2(AB + AC + BC)

2(AB + AC + BC) = (A + B + C)² – (A² + B² + C²)

Combining gives:

]]>16T² = (A + B + C)² – 2(A² + B² + C²)

Then there are 193 zones (we will number them from 0 to 192), we want to avoid being in the even numbered zones at t=0 and the odd numbered zones at t=12.

So, if we were in the middle of an even zone at t=12 we would be safe, and if we could get to the middle of the next odd zone by t = 0 we would be safe in that zone.

That requires travelling 19.1 yards every 12 seconds. Which is 95.5 yards/minute, which is not a permitted speed.

But it is in the right range of available speeds, so we can look at travelling 95 yards/minute or 96 yards/minute and see if they work.

If we can get from the middle of zone 96 at t=12 to the end of the final zone without being caught, then, by symmetry, we would be able to get to the middle of zone 96 without getting caught.

In the following Python program I express distances in “centiyards” (i.e. 1/100 of a yard). It runs in 107ms.

**Run:** [ @repl.it ]

from fractions import Fraction as F from enigma import printf # total distance, half distance D = 368630 H = F(D, 2) # check if travelling at speed <v0>, starting at distance <d0>, time <t0> allows passage def check(v0, d0, t0): # speed in centiyards per second (v, d, t) = (F(v0, 60), d0, t0) # where are we at the next multiple of 12? x = -t % 12 if x > 0: t += x d += v * x while d < D: # are we caught? (b, z) = (t // 12, d // 1910) if (b % 2) == (z % 2): printf("v={v:.2f} yard/min, t={t0:.2f}, d={d0}: caught at t={t}, z={z}, d={d:.2f}", v=float(v0) * 0.01, t0=float(t0), d=float(d)) return False # move on 12s t += 12 d += v * 12 # we made it printf("v={v:.2f} yard/min, t={t:.2f}, d={d0}: not caught", v=float(v0) * 0.01, t=float(t0)) return True # check 95.5 yards/min and permissible close speeds for v in [9550, 9500, 9600]: check(v, H, 12)

**Solution:** You should travel at 96 yards/minute.

To pass exactly through the centre of zone 96 at t=12 the required start time is t=12 + 1/32 seconds (= 12.03125s).

And 96 yards/minute is the only permissible whole number speed in the allowed range that permits an unobserved passage:

% python3.7 -i enigma517b.py >>> list(v for v in irange(100, 17000, step=100) if check(v, H, 12)) [9600]]]>

Both numbers are equal to 3,628,800, but we don’t need to work out the values of the numbers.

We can write 6 weeks as:

]]>6 weeks =

(6 × 7) days =

(6 × 7 × 24) hours =

(6 × 7 × 24 × 60) minutes =

(6 × 7 × 24 × 60 × 60) seconds =

(6 × 7 × 24 × 3600) seconds =

(6 × 7 × 24 × (36 × 100)) seconds =

(6 × 7 × (3 × 8) × ((4 × 9) × (2 × 5 × 10)) seconds =

(2 × 3 × 4 × 5 × 6 × 7 × 8 × 9 × 10) seconds =

factorial(10) seconds

Q1 and Q2 are relatively straightforward, Q3 is more convoluted.

This Python 3 program uses the [[ `Football()` ]] helper class from the **enigma.py** library. It runs in 472ms.

**Run:** [ @repl.it ]

from collections import defaultdict from functools import cmp_to_key from enigma import Football, subsets, unpack, Accumulator, multiset, irange, update, compare, join, args, printf # logical implication: p -> q implies = lambda p, q: not(p) or q # scoring system football = Football(games='wdl', points=dict(w=3, d=1)) # labels for the teams teams = (A, B, C, D) = tuple("ABCD") # keys for the matches ks = list(x + y for (x, y) in subsets(teams, size=2)) # consider possible match outcomes rs = list() for ms in football.games(repeat=len(ks)): ms = dict(zip(ks, ms)) # compute the points for each team pts = dict((t, football.extract_table(ms, t).points) for t in teams) # how many teams did better than A better = sum(pts[x] > pts[A] for x in teams if x != A) # record the outcome (matches, better) rs.append((ms, better)) # find cases where if we know the outcomes of matches in <ks>, then (AvC = lose) -> (no prize) def extract(ks): f = unpack(lambda ms, better: tuple(ms[k] for k in ks)) g = unpack(lambda ms, better: implies(ms["AC"] == 'l', better > 1)) d = defaultdict(set) for r in rs: d[f(r)].add(g(r)) # look for keys that only give True for (k, v) in d.items(): if v != set([True]): continue # select outcomes with this key, and where AvD = lose and A wins a prize h = unpack(lambda ms, better: ms["AD"] == 'l' and better < 2) for r in rs: if f(r) == k and h(r): yield r[0] # question 1 def q1(): # record number of viable outcomes ans = 0 # match 1 is AvB, match 3 is AvC # match 2 is one of BvC, BvD, CvD for k2 in ["BC", "BD", "CD"]: for ms in extract(["AB", k2]): printf("[q1: k2={k2} -> {ms}]") ans += 1 printf("q1: {x}", x=("possible" if ans > 0 else "not possible")) printf() # question 2 def q2(): # look for matches that appear in all viable outcomes ans = Accumulator(fn=lambda s, x: s.intersection(x)) # match 1 is AvB, match 4 is AvC # consider possible 2nd, 3rd matches from BvC, BvD, CvD for (k2, k3) in subsets(["BC", "BD", "CD"], size=2): for ms in extract(["AB", k2, k3]): printf("[q2: k2={k2}, k3={k3} -> {ms}]") ans.accumulate(set([k2, k3])) printf("q2: {ans}", ans=(join((x + "v" + y for (x, y) in sorted(ans.value)), sep=" ") if ans.value else "<none>")) printf() # generate scores for matches ms, with t total goals scores def scores(ms, t, ss=dict()): if not ms: if t == 0: yield ss else: # choose a match ms = dict(ms) (k, v) = ms.popitem() if v in 'wl': for x in irange(1, t): for y in irange(0, min(x - 1, t - x)): s = ((x, y) if v == 'w' else (y, x)) yield from scores(ms, t - x - y, update(ss, [k], [s])) elif v in 'd': for x in irange(0, t // 2): yield from scores(ms, t - x - x, update(ss, [k], [(x, x)])) # compute the order of the teams given match outcomes <ms> and scores <ss> def order(ms, ss): # calculate the points pts = dict((t, football.extract_table(ms, t).points) for t in teams) # calculate goals (for, against) goals = dict((t, football.extract_goals(ss, t)) for t in teams) # compare performance in game x vs. y def game(x, y): if x < y: (fx, fy) = ss[x + y] else: (fy, fx) = ss[y + x] return compare(fx, fy) # compare teams on points, then goal difference, then goals scored, then performance in the match between them cmp = lambda x, y: ( compare(pts[x], pts[y]) or compare(goals[x][0] - goals[x][1], goals[y][0] - goals[y][1]) or compare(goals[x], goals[y]) or game(x, y) ) # order the teams (winner first) return sorted(teams, key=cmp_to_key(cmp), reverse=1) # question 3 def q3(): # look for scores in the BvC match ans = multiset() # match 1 is AvB # and before AvC is played some subset of BvC, BvD, CvD are also played for ks in subsets(["BC", "BD", "CD"]): for ms in extract(["AB"] + list(ks)): # but there can't be more than 4 matches won/lost if sum(m in 'wl' for m in ms.values()) > 4: continue # find possible scores for each game for ss in scores(ms, 4): # and check A wins a prize r = order(ms, ss) if r.index('A') > 1: continue printf("[q3: ks={ks} -> {ss} {r}]") ans.add((ms["BC"], ss["BC"])) for ((m, s), n) in ans.most_common(): printf("q3: BvC = {m}:{s} [{n} solutions]") printf() arg = join(args("123", 0)) if "1" in arg: q1() if "2" in arg: q2() if "3" in arg: q3()

**Solution:** (1) No, it is not possible that A vs C was the third game of the tournament. (2) The C vs D match must have been played as the second or third game. (3) The score in the B vs C match was 0-0.

**Run:** [ @repl.it ]

from enigma import tuples, subsets, printf # Pukka - always tells the truth def P(ss): return all(ss) # Wotta-Woppa - never tells the truth def W(ss): return not any(ss) # Shilli-Shalla - alternates between true and false def S(ss): return all(a ^ b for (a, b) in tuples(ss, 2)) # assign tribes to the candidates for (A, B, C, D) in subsets((P, W, S), size=4, select="M"): # there must be at least one of each tribe if not all((A, B, C, D).count(x) > 0 for x in (P, W, S)): continue # assign possible jobs for (jA, jB, jC, jD) in subsets(['DO', 'DS', 'WO', 'BW'], size=4, select="P"): # check the statements: # B: A's tribe is P; C's job is WO if not B([A == P, jC == 'WO']): continue # C: C's tribe is W; D's job is DS if not C([C == W, jD == 'DS']): continue # map jobs to tribes t = dict(zip([jA, jB, jC, jD], [A, B, C, D])) # D: DO's tribe is S; C's tribe is not W if not D([t['DO'] == S, C != W]): continue # A: WO's tribe is W; only one P if not A([t['WO'] == W, (A, B, C, D).count(P) == 1]): continue f = lambda x: x.__name__ printf("A={jA}:{A} B={jB}:{B} C={jC}:{C} D={jD}:{D}", A=f(A), B=f(B), C=f(C), D=f(D))

**Solution:** Alf is the Welfare Officer, he is a Shilli-Shalla; Bert is the Bottle Washer, he is a Wotta-Woppa; Charlie is the Door-Opener, he is a Shilli-Shalla; Duggie is the Door-Shutter, he is a Pukka.

The following run file executes in 102ms.

**Run:** [ @repl.it ]

#!/usr/bin/env python -m enigma -r SubstitutedDivision "qsmcaq / csm = ween" "qsm - csm = wbb" "wbbc - nhn = wea" "weaa - nhn = snn" "snnq - sqwe = shs"

**Solution:** The correct sum is: 539465 ÷ 439 = 1228 (remainder 373).

Then we can assign a value to a word by summing the values for each of it’s letters:

CAESAR = 3 + 1 + 5 + 19 + 1 + 18 = 47

But we didn’t really get “from 3 to 47” (unless you count the fact that the first letter of Caesar has a value of 3).

However, the title of the puzzle is “Caesar cipher”. A Caesar cipher is a simple substitution code where text is encoded by shifting each letter along the alphabet by a fixed number of steps [ @Wikipedia ].

The key is the number of steps to shift.

If we start with the plaintext “THREE” and shift each letter along the alphabet by four steps (key = 4) we get:

THREE → XLVII

and XLVII is the Roman Numeral representation of the number 47.

Which I think is a neater way of using Caesar to get from 3 to 47.

]]>It worked OK, but could not easily derive it from Heron’s formula.

% A Solution in MiniZinc include "globals.mzn"; var int: T = 7; % triangular lake area % three square field areas on each side of lake var 1..50: A; var 1..50: B; var 1..50: C; % using Jim's variant of Heron’s Formula constraint 16*T*T = (A+B+C)*(A+B+C) - 2*(A*A + B*B + C*C); % smallest possible total area of the three fields solve minimize(A+B+C); output ["Minimum area of three fields = " ++ show(A+B+C) ++ " acres" ++ "\nIndividual three fields are " ++ show(A) ++ ", " ++ show(B) ++ " and " ++ show(C) ++ " acres"]; % Minimum area of three fields = 50 acres % Individual three fields are 17, 13 and 20 acres % ---------- % ==========]]>

16T² = (A + B + C)² – 2(A² + B² + C²)

This Python program runs in 114ms.

**Run:** [ @repl.it ]

from enigma import irange, first, printf # decompose total t into k numbers, min value m def decompose(t, k, m=1, s=[]): if k == 1: yield s + [t] else: for x in irange(m, t // k): yield from decompose(t - x, k - 1, x, s + [x]) # find solutions for the specified area def solve(T): T *= 16 * T # decompose the total area into three whole numbers for t in irange(3, irange.inf): for (A, B, C) in decompose(t, 3): # calculate 16.T^2 m = (A + B + C) ** 2 - 2 * (A ** 2 + B ** 2 + C ** 2) # have we found the desired area? if m == T: yield (t, A, B, C) # find the first (smallest) solution for (t, A, B, C) in first(solve(7), 1, fn=iter): printf("t={t}, A={A} B={B} C={C}")

**Solution:** The smallest possible total area of the fields is 50 acres.

The fields have areas of 13, 17 and 20 acres.

]]>**Run:** [ @repl.it ]

from enigma import subsets, join, printf # wives and husbands wives = list("ABCDE") husbands = list("PQRST") # make a pairing map (x -> y, y -> x) def pairs(xs, ys): d = dict(zip(xs, ys)) d.update(zip(ys, xs)) return d # make the second marriage pairing for s1 in subsets(husbands, size=len, select="P"): m2 = pairs(s1, wives) # collect possible orderings for the weddings ws = list() for s3 in subsets(wives, size=len, select="P"): w = dict((x, i) for (i, x) in enumerate(s3, start=1)) # ordering constraints # "D was not the first to remarry" if w['D'] == 1: continue # "P's wedding was earlier than A's and later than Q's if not(w[m2['Q']] < w[m2['P']] < w['A']): continue # "B's wedding was later than T's and earlier than C's if not(w[m2['T']] < w['B'] < w['C']): continue ws.append(w) if not(ws): continue # make the first marriage pairing for s2 in subsets(husbands, size=len, select="P"): m1 = pairs(s2, wives) # no-one keeps their partner if any(m1[x] == m2[x] for x in wives): continue # no-one makes a direct swap if any(m2[m1[m2[m1[x]]]] == x for x in wives): continue # we know some constraints between first and second marriages # "P married S's ex-wife" if not(m2[m1['S']] == 'P'): continue # "B married the man whose former wife married E's ex-husband" if not(m1[m2['B']] == m2[m1['E']]): continue # "Q married the lady whose former husband married D" if not(m1[m2['Q']] == m2['D']): continue # "R married the lady whose ex-husband married C" if not(m2[m1[m2['R']]] == 'C'): continue # output solution for x in wives: printf("{x}: divorced {d}, married {m}", d=m1[x], m=m2[x]) printf("[order = {ws}]", ws=join((join(sorted(w.keys(), key=(lambda k: w[k]))) for w in ws), sep=", ")) printf()

**Solution:** The pairings are: Emily and Quentin, Dinah and Tristram, Barbara and Ronald, Celia and Peter, Anne and Simon.

And these are given in the order of the weddings (starting with the earliest).

The previous pairings were:

]]>Anne and Quentin

Barbara and Peter

Celia and Simon

Dinah and Ronald

Emily and Tristram

So we see that there must be *at least* 5 dancers, for the ribbons to have to cross.

And as they had arranged themselves in such a way that no three of the dancers that were interconnected by ribbons of the same colour, there must have been fewer than 6 dancers. (By **Ramsey’s Theorem** [ @Wikipedia ] it is not possible to colour a graph will 6 or more elements without creating a 3-set interconnected by the same colour).

There is essentially only one way to construct the graph. So we need to find a 5-set of dancers, such that each dancer has an odd number of letters in common with exactly two of his fellow dancers.

This Python program uses the [[ `multiset()` ]] class (recently added to the **enigma.py** library) to count the letters shared between names.

It runs in 177ms.

**Run:** [ @repl.it ]

from enigma import subsets, multiset, join, printf # names names = "TOM VYCTUR TED TAGO RAY MIN WEX OLAV RUSS CY".split() # colour the edge between x and y def colour(x, y): return len(multiset(x).intersection(y)) % 2 # count the colourings of edges between n and ns def count(n, ns): r = [0, 0] for x in ns: if x == n: continue r[colour(n, x)] += 1 return tuple(r) # choose a 5-set of names for ns in subsets(names, size=5): # check each member has 2 ends of each of the 2 colours if all(count(n, ns) == (2, 2) for n in ns): printf("{ns}", ns=join(ns, sep=" "))

**Solution:** The dancers were: Tom, Ted, Tago, Ray, Olav.

We can draw an arrangement like this, where no set of three dancers is interconnected by three ribbons of the same colour:

But we can also draw an arrangement like this, where none of the triangles formed in the diagram have three sides the same colour:

]]>**Run:** [ @repl.it ]

from collections import defaultdict from enigma import divisor_pairs, printf # find ordered k-tuples that multiply to give n def divisor_tuples(n, k, s=[]): if k == 1: if not(s and n < s[-1]): yield s + [n] else: for (a, b) in divisor_pairs(n): if not(s and a < s[-1]): yield from divisor_tuples(b, k - 1, s + [a]) # looks for divisor triples of 2450 # record the results by the sum of the divisors r = defaultdict(list) for s in divisor_tuples(2450, 3): r[sum(s)].append(s) # look for ambiguous values for (k, vs) in r.items(): if len(vs) < 2: continue # sort the sets of ages by their oldest age vs = sorted(vs, key=max) # vicar must be older than the eldest in the first group # but not older than the eldest in the next group v = [max(vs[0]) + 1, max(vs[1])] # output solution printf("bishop = {k}, vicar = {v}, ages = {vs}")

**Solution:** The vicar is 50.

The bishop must be 64, which gives the following possible factorisation of 2450:

2450 = 5×10×49; 5+10+49 = 64

2450 = 7×7×50; 7+7+50 = 64

So, to narrow down the possibilities to a single option the vicar must be 50, and the family must have ages: 5, 10, 49.

]]>