It runs in 85ms.

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

from enigma import tuples, irange, Accumulator, printf # the adjacency matrix for the graph adj = { 'A': 'B', 'B': 'ACD', 'C': 'B', 'D': 'BFH', 'E': 'F', 'F': 'DEG', 'G': 'F', 'H': 'DJM', 'I': 'J', 'J': 'HIK', 'K': 'J', 'L': 'M', 'M': 'HLO', 'N': 'O', 'O': 'MNPR', 'P': 'O', 'Q': 'R', 'R': 'OQS', 'S': 'R', } # given distances distance = dict(AB=6, DH=4, FG=6, HJ=8, HM=3, JK=5, LM=10, MO=8, OP=5, QR=2) # nodes nodes = sorted(adj.keys()) # calculate the paths between any two nodes paths = list() ps = list(nodes) while ps: p = ps.pop(0) for t in adj[p[-1]]: if t not in p: x = p + t ps.append(x) if p[0] < t: paths.append(x) key = lambda x, y: (x + y if x < y else y + x) # extract known and unknown values def path(p, d): (vs, us) = (list(), list()) for (x, y) in tuples(p, 2): k = (x + y if x < y else y + x) x = d.get(k, None) (us if x is None else vs).append(k) return (tuple(sorted(vs)), tuple(sorted(us))) # recorded maximum possible distance for combinations of unknowns ms = dict() # no village is more than 20 miles from D, or 29 miles from P for (t, z) in [('D', 20), ('P', 29)]: for p in paths: if p[0] == t or p[-1] == t: (vs, us) = path(p, distance) m = z - sum(distance[k] for k in vs) printf("path = {p}: vs={vs} us={us}, z={z} m={m}") if us: if us not in ms or m < ms[us]: ms[us] = m # record maximum path r = Accumulator(fn=max) # find undetermined paths uns = list() for p in paths: (vs, us) = path(p, distance) t = sum(distance[k] for k in vs) if us: uns.append((p, vs, us, t)) else: r.accumulate_data(t, p) printf("max known path {r.data} = {r.value}") # find possible distances for unknown segments def dists(us, t=0): # are we done? if not us: yield t else: # find the best fit unknowns k = max(ms.keys(), key=lambda k: (len(us.intersection(k)), -len(set(k).difference(us)))) # choose a value for the unknowns for v in irange(len(us.intersection(k)), ms[k] - len(set(k).difference(us))): # solve for the remaining unknowns yield from dists(us.difference(k), t + v) # find maximum possible undetermined path for (p, vs, us, t) in uns: v = max(dists(set(us), t)) r.accumulate_data(v, p) if v == r.value: printf("path {p} = {v}") printf("max path {r.data} = {r.value}")

**Solution:** The maximum distance between any two villages is 29 miles.

The path from K to P has a known length of 29 miles, and this is longest of any known path.

While there are several paths of unknown length, none of them can exceed 29 miles. However any of the paths from (A, C, E, G, I, K) to (N, P, Q, S) can equal 29 miles.

For instance if K to S was greater than 29, then O to S would have to be greater than 5. But then D to S would be greater than 20 miles, which is not possible.

]]>I represented the individual sins as different binary bits in a 7-bit integer, then I can use the technique from the **Bit Twiddling** article to generate pairs of sins, and it makes it easy to check if people have sins in common, or to check the set of sins corresponding to a group of people.

I also wrote the [[ `choose()` ]] function which works a bit like [[ `itertools.permutations()` ]], but instead of specifying the number of items required, a list of functions is specified that is used to select the next choice. This saves having to write a bunch of nested [[ `for` ]] loops.

This Python 3 program runs in 85ms.

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

from enigma import irange, bit_permutations, join, printf # the sins (as bit values) sins = (Sl, Lu, Pr, Av, In, An, En) = tuple(1 << i for i in irange(0, 6)) # pairs of sins (7-bit integers with 2 bits set) # see: https://enigmaticcode.wordpress.com/2017/05/20/bit-twiddling/ pairs = list(bit_permutations(3, 128)) # choose values from <vs> satisfying <fns> in turn # set 'distinct' if values must be distinct def choose(vs, fns, s=[], distinct=0): # are we done? if not fns: yield s else: # choose the next value fn = fns[0] for v in vs: if not(distinct) or v not in s: ss = s + [v] if fn is None or fn(*ss): # choose the rest yield from choose(vs, fns[1:], ss, distinct) # check <n> has sin <s> has = lambda n, s: bool(n & s) hasnt = lambda n, s: not has(n, s) # check each sin occurs <k> times in <vs> def check(vs, k=2): return all(sum(has(v, s) for v in vs) == k for s in sins) # choose sin values in order D, C, F, E, B, G, A fns = ( # D has Lust, but not Anger lambda D: has(D, Lu) and hasnt(D, An), # C has anger # C and D have a sin in common lambda D, C: has(C, An) and (C & D), # F has not Pride, not Intemperance # C and F have no sins in common lambda D, C, F: hasnt(F, Pr) and hasnt(F, In) and not(C & F), # E has Lust # C and E, E and F have no sins in common lambda D, C, F, E: has(E, Lu) and not(C & E) and not(E & F), # B has not Avarice lambda D, C, F, E, B: hasnt(B, Av), # G has Sloth # B, D, E, G have all 7 sins between them lambda D, C, F, E, B, G: has(G, Sl) and (B | D | E | G) == 0b1111111, # A has Sloth and not Pride # each sin occurs twice lambda D, C, F, E, B, G, A: has(A, Sl) and hasnt(A, Pr) and check((A, B, C, D, E, F, G), 2), ) # labels for the names and the sins t_names = ( 'Alice', 'Beatrice', 'Constance', 'Deborah', 'Emily', 'Flavia', 'Gertrude' ) t_sins = ( 'Sloth', 'Lust', 'Pride', 'Avarice', 'Intemperance', 'Anger', 'Envy' ) # format sins fmt = lambda n: join((t for (v, t) in zip(sins, t_sins) if has(n, v)), sep=", ") # find assignments of sins to people for (D, C, F, E, B, G, A) in choose(pairs, fns, distinct=1): # output solution for (s, t) in zip((A, B, C, D, E, F, G), t_names): printf("{t:9s} : {s}", s=fmt(s)) printf()]]>

I looked at different ways of dividing a rectangle into 3 triangles (where the smallest 2 are right-angled), and then dividing the largest triangle into 3 more right-angled triangles, each having the same area.

My first attempt was this:

To get them to fit each triangle has a smallest angle of 30°, so they are all similar (and the green ones are congruent).

Which makes the base of the rectangle √(3) times the height.

Obviously the three green triangles make up 1/2 (50%) the area (so 1/6 (16.7%) each), and together they are larger than either of the remaining triangles.

This gives the pink triangle an area of 3/8 (37.5%) and the blue triangle an area of 1/8 (12.5%), so individually the green ones aren’t smaller than the blue one.

::

My next attempt was this:

It turns out this is just a rearrangement of the triangles above into a different rectangle.

In this case the base is (4/3)√(3) the height, but it has the same problem as the first attempt.

::

This was my third go, which has the big green triangle bite in to the rectangle from the side instead of the top:

This time the green triangles are not congruent (although they do all have the same area, and it is still 1/6 (16.7%) of the rectangle, so together they still make up half the rectangle), and the big green triangle is not right-angled.

But the blue triangle is 1/5 (20%) of the rectangle and the pink triangle is 3/10 (30%) of the rectangle, so this gives us a solution.

The base of the triangle is 6/5 the height.

**Solution:** The longer side of the map measures 72 cm.

The lowest common multiple of 3, 4, 5 is 60, so if there was a floor 60, all the lifts would stop there. So there must be fewer that 60 floors.

This Python program chooses floors for Amble, Bumble and Crumble, and possible matching floors for the drummer, flautist, harpist and trombonist. The unmatched floor number must then belong to Dimwit. It runs in 81ms.

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

from enigma import irange, is_distinct, diff, printf # minimum sets of stairs to walk down between floors from a to b def down(a, b): # go down to the first lift below a, and then down from the first lift above b x = min(a % 3, a % 4, a % 5) + min(-b % 3, -b % 4, -b % 5) # or just walk down y = a - b return (y if not(y < 0) and y < x else x) # "None of the four lives on a floor served by a lift" # so floors that are multiples of 3, 4, 5 are out Floor = set(n for n in irange(0, 59) if all(n % d != 0 for d in (3, 4, 5))) # choose a floor for Amble for A in Floor: # "Amble has no way of visiting the trombonists flat without walking down at least four sets of stairs" for t in Floor: if down(A, t) < 4: continue # "Amble and the drummer are on adjacent floors" for d in (A - 1, A + 1): if not(d in Floor and is_distinct(d, t)): continue # choose a floor for Bumble for B in Floor: if not is_distinct(A, B): continue # "The harpist lives four floors above Bumble" h = B + 4 if not(h in Floor and is_distinct(h, t, d)): continue if not(len(set((A, B, t, d, h))) < 5): continue # choose a floor for Crumble for C in Floor: if not is_distinct(C, A, B): continue # "Crumble and the flautist are ten floors apart" for f in (C - 10, C + 10): if not(f in Floor and is_distinct(f, t, d, h)): continue # so D lives at whatever role based floor is unused D = diff((d, f, h, t), (A, B, C)) if not(len(D) == 1): continue D = D[0] # output solution (highest to lowest) # f2r: Floor -> Role f2r = { d: "Drummer", f: "Flautist", h: "Harpist", t: "Trombonist" } for (k, n) in sorted(zip((A, B, C, D), ("Amble", "Bumble", "Crumble", "Dimwit")), reverse=True): printf("{n}, {r}, floor {k}", r=f2r[k]) printf()

**Solution:** Dimwit plays the harp and lives on floor 26.

The full solution is:

Dimwit, Harpist, floor 26

Amble, Flautist, floor 23

Bumble, Drummer, floor 22

Crumble, Trombonist, floor 13

Amble can walk down 2 sets of stairs from floor 23 to floor 21, then take the 3-lift down to floor 15, and then walk down 2 more sets of stairs to reach the trombonist on floor 13.

In fact there are no two floors that require Amble to walk down more than 4 sets of stairs, as you can never be more than two floors away from a lift. So, there are only 6 floors below 60 that Amble can live on: 2, 14, 23, 38, 47, 59.

]]>If we suppose the “played” and “points” columns are correct.

If no matches had been played, all the values would be 0, so there would be more than one mistake in the table (and in particular the columns we are supposing are correct).

If all matches had been played, then every value in the “played” column would be 2, so there would be a mistake in the “played” column.

If only one match had been played then *k* and *g* would be *0* and *1* (in some order). But they appear in the other order in the “points” column. So one team would have to have played 0 matches and gained 1 point. This is impossible.

If two matches had been played. Then one team has played 2 matches, and the other teams have played 1 match each. So *k* and *g* would be *1* and *2* in some order. In each of the 2 matches 2 points are awarded, so the sum of the “points” column is 4, so *m* would have to be *1*, but we know one of *g* or *k* is *1*. So this is not possible either.

So there is no scenario where both the “played” and “points” columns are correct, so the error must be in one of these columns.

This Python program uses the [[ `Football()` ]] helper class from the **enigma.py** to find the erroneous entry in the table, and the outcomes of the matches. It runs in 83ms.

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

from itertools import permutations from enigma import Football, irange, chunk, seq_all_different, update, printf # scoring system football = Football(games='wdlx', points=dict(w=2, d=1)) # the columns of the table (without the goals columns) # played points won lost drawn table = "kg?" + "gkm" + "??m" + "m??" + "??k" # possible digits digits = set(irange(0, 9)) # remove one of the entries from the played/points columns for i in (0, 1, 3, 4, 5): t = table[:i] + '?' + table[i + 1:] # split out the revised table into columns t = dict(zip(('played', 'points', 'w', 'l', 'd'), chunk(t, 3))) for (m, d) in football.substituted_table(t): # now solve the sum ds = digits.difference(d.values()) for (h, x) in permutations(ds, 2): (p, t) = (h + x, 2 * x) vs = (h, x, p, t) if not(seq_all_different(vs) and ds.issuperset(vs)): continue # determine possible scorelines (using the goals columns) d1 = update(d, 'hxpt', vs) for s in football.substituted_table_goals('xpx', 'htx', m, d1): # output solution printf("[error @ i={i} is {v}]", v=table[i]) football.output_matches(m, s, teams="ABC", d=d1)

**Solution:** The points entry for C should be *k* (= 1), not *m* (= 0). The scores in the matches are: A v B = 4 – 3; A v C = not played; B v C = 4 – 4. The addition sum is: 3 + 4 = 7.

Q1 & Q2: B is known, and there is an upper bound on the total.

Q3 & Q4: C and E are known.

By manipulating the inequalities we can come up with bounded expressions for the unknown variables.

Here is a Python program bases on this approach. It runs in 75ms.

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

from enigma import irange, divc, divf, printf # solve for a given B and T < M: # # 3B / 2 < A # (2A - B) / 4 < C < A - B # (2A + 2B - C) / 3 < D < B + C # A + B - C - D < E < (D - C) / 2 # (C + D) / 2 < F < D - E # C + D + E - F < G < E + F # def solve1(q, B, M): for A in irange(divc(3 * B + 1, 2), M - B - 4): for C in irange(divc(2 * A - B + 1, 4), min(A - B - 1, M - A - B - 3)): for D in irange(divc(2 * (A + B) - C + 1, 3), min(B + C - 1, M - A - B - C - 3)): for E in irange(max(A + B - C - D + 1, 0), min(divf(D - C - 1, 2), M - A - B - C - D - 1)): for F in irange(divc(C + D + 1, 2), min(D - E - 1, M - A - B - C - D - E - 1)): for G in irange(C + D + E - F + 1, min(E + F - 1, M - A - B - C - D - E - F - 1)): T = A + B + C + D + E + F + G printf("[{q}] A={A} B={B} C={C} D={D} E={E} F={F} G={G}, T={T}") # solve for given C and E: # # C + 2E < A < 2C + E # 2E < B < A - C # C + 2E < D < B + C # (C + D) / 2 < F < D - E # C + D + E - F < G < E + F # def solve2(q, C, E): for A in irange(C + 2 * E + 1, 2 * C + E - 1): for B in irange(2 * E + 1, A - C - 1): for D in irange(C + 2 * E + 1, B + C - 1): if not(A + B < C + D + E): continue for F in irange(divc(C + D + 1, 2), D - E - 1): for G in irange(C + D + E - F + 1, E + F - 1): T = A + B + C + D + E + F + G printf("[{q}] A={A} B={B} C={C} D={D} E={E} F={F} G={G}, T={T}") solve1("Q1", 4, 75) solve1("Q2", 5, 65) solve2("Q3", 107, 100) solve2("Q4", 108, 100)

Note that in the standard implementation of Python you cannot go on nesting [[ `for` ]] loops indefinitely. There is a hard coded limit of 20 nested blocks in the Python interpreter. Although this restriction does not seem to apply to the *PyPy* implementation.

This Python 3 program executes in 469ms.

from minizinc import MiniZinc from enigma import join, printf def solve(name, condition, solver="mzn-gecode -a"): model = f""" % count the number of visits var int: A; % months [1,2,3] var int: B; % months [4,5] var int: C; % months [6] var int: D; % months [7,8,9] var int: E; % months [10] var int: F; % months [11,12] var int: G; % months [13,14,15] var int: T = A + B + C + D + E + F + G; % total % number of visits is non-negative constraint forall (x in [A, B, C, D, E, F, G]) (not(x < 0)); % S calculates the number of visits is increasing, using 5 month chunks constraint A + B < C + D + E /\ C + D + E < F + G; % D calculate the number of visits is decreasing, using 3 month chunks constraint A > B + C /\ B + C > D /\ D > E + F /\ E + F > G; % additional conditions constraint {condition}; solve satisfy; """ p = MiniZinc(model, solver=solver) n = 0 for (A, B, C, D, E, F, G) in p.solve(result="A B C D E F G"): T = A + B + C + D + E + F + G printf("{name}: T={T} [A={A} B={B} C={C} D={D} E={E} F={F} G={G}]") n += 1 if n == 0: printf("{name}: UNSATISFIABLE") return n solve("Q1", "B = 4 /\ T < 75") solve("Q2", "B = 5 /\ T < 65") solve("Q3", "C = 107 /\ E = 100") solve("Q4", "C = 108 /\ E = 100")

**Solution:** (1) No; (2) Yes. 60 total visits; (3) No; (4) Yes. 1560 total visits.

I split the time period into:

A = Jan 98, Feb 98, Mar 98

B = Apr 98, May 98

C = Jun 98

D = Jul 98, Aug 98, Sep 98

E = Oct 98

F = Nov 98, Dec 98

G = Jan 99, Feb 99, Mar 99

Then S sees increasing visits using 5 month chunks:

A + B < C + D + E < F + G

and D sees decreasing visits using 3 month chunks:

A > B + C > D > E + F > G

There is a solution for question (2) when:

A=14 B=5 C=8 D=12 E=0 F=11 G=10, total = 60

Which gives:

19 < 20 < 21

14 > 13 > 12 > 11 > 10

Adding 100 visits to each month then gives a solution for question (4):

A=314 B=205 C=108 D=312 E=100 F=211 G=310, total = 1560

and these are the only solutions.

]]>The main advantage is that an element knows it’s own name, which makes it easier to do the output. The main disadvantage is the need to qualify the elements with type name.

Here I’ve used [[ `IntEnum()` ]] so I can use the values as indices into arrays.

This Python program runs in 89ms.

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

from enum import IntEnum from itertools import permutations from enigma import printf # names of the employees Name = IntEnum("Name", "Alf Bert Charlie Duggie Ernie", start=0) # jobs Job = IntEnum("Job", "Worker DoorKnobPolisher DoorOpener DoorShutter BottleWasher", start=0) # map employees to positions (1 to 5) # position: names -> (1 to 5) for position in permutations((1, 2, 3, 4, 5)): # 3. "Alf's place was even..." if not(position[Name.Alf] % 2 == 0): continue # map employees to jobs # job: names -> jobs for job in permutations(Job): # 1. "Bert was placed as many places below the Worker as he was above the Door-Knob-Polisher" if not(position[Name.Bert] - position[job.index(Job.Worker)] == position[job.index(Job.DoorKnobPolisher)] - position[Name.Bert] > 0): continue # 2. "The Door-Opener was three places above Charlie" if not(position[job.index(Job.DoorOpener)] + 3 == position[Name.Charlie]): continue # 3. "... the Door-Shutters place was odd" if not(position[job.index(Job.DoorShutter)] % 2 == 1): continue # 4. "The Bottle-Washer was two places above Ernie" if not(position[job.index(Job.BottleWasher)] + 2 == position[Name.Ernie]): continue # output a solution for (p, n, j) in sorted(zip(position, Name, job)): printf("{p}: {n} ({j})", n=n.name, j=j.name) printf()

**Solution:** 1st: Duggie (Door Opener); 2nd: Alf (Worker); 3rd: Bert (Bottle Washer); 4th: Charlie (Door Knob Polisher); 5th: Ernie (Door Shutter).

I know it makes no difference to the sum, but the way the puzzle is arranged looks odd. ]]>

The following run file executes in 156ms.

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

#!/usr/bin/env python -m enigma -r SubstitutedExpression --answer="IMPINGE" # upper case symbols are distinct --distinct="AEGIMNP" # the main multiplication sum "{xyz} * {GAP} = {ENIGMA}" # the intermediate multiplications "{xyz} * {G} = {abc}" "{xyz} * {A} = {def}" "{xyz} * {P} = {ghNi}" # GAP is a multiple of 9 "GAP % 9 = 0"

**Solution:** IMPINGE = 5285061.

And A=4.

The multiplication sum is: 163 × 648 = 105624.

Although it is not necessary to solve the puzzle, the extra [[ `"GAP % 9 = 0"` ]] expression saves about 25ms of CPU time.

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

from collections import defaultdict, Counter from itertools import product, combinations from enigma import irange, join, seq_all_same as all_same, seq_all_different as all_different, flatten, printf # record squares (as strings) by number of digits squares = defaultdict(list) for i in irange(0, 99): s = str(i * i) squares[len(s)].append(s) # format a set of squares fmt = lambda x: "{" + join(x, sep=", ") + "}" # find sets of 1-, 2-, 3- and 4-digit square with the required conditions ss = list() for s in product(squares[1], squares[2], squares[3], squares[4]): t = join(s) # digit 7 must be included if not('7' in t): continue # each digit must be used exactly twice c = Counter(t) if not all_same(c.values(), value=2): continue ss.append(s) printf("[{s}]", s=fmt(s)) # choose a set of 4 solutions with no elements in common for s in combinations(ss, 4): if all_different(flatten(s)): printf("{s}", s=join(map(fmt, s), sep=", "))

**Solution:** The solution that included 9 is: 9, 25, 529, 7744.

There are two possible sets of solutions:

0, 49, 729, 2704

1, 16, 225, 5776

4, 64, 576, 7225

9, 25, 529, 7744

and:

0, 49, 729, 2704

1, 16, 576, 7225

4, 64, 225, 5776

9, 25, 529, 7744

In both cases the squares that go with 0 and 9 are the same. The solutions with 1 and 4 can swap their 3- and 4- digit squares to give the other set of solutions.

Altogether there are 14 different sets of 4 squares that can be constructed.

]]>0, 49, 729, 2704

1, 16, 225, 5776

1, 16, 576, 7225

1, 36, 361, 7744

4, 25, 576, 6724

4, 49, 576, 7569

4, 64, 225, 5776

4, 64, 576, 7225

9, 16, 169, 7744

9, 16, 196, 7744

9, 16, 961, 7744

9, 25, 529, 7744

9, 49, 576, 5476

9, 64, 729, 6724

>>> bungled_sum(["YTBBEDMKD", "YHDBTYYDD", "EDYTERTPTY"], [2]) T YTBBEDMKD + YHDBTYYDD = EDYTERTPHY / @[2,8] T -> H 695513243 + 673596633 = 1369109876 / B=5 D=3 E=1 H=7 K=4 M=2 P=8 R=0 T=9 Y=6 [bungled_sum] elapsed time: 0.2055500s (205.55ms)]]>

Grunts tell the truth, we will label them G. Phews lie, we will label them P.

So a Grunt would say: “I am G”. And a Phew would say: “I am G”. So we can’t tell who is questioned first.

If the queue went: GG…, the second would say: “I am G, previous was G”. This is not the case.

If the queue went: PG…, the second would say: “I am G, previous was P”. This is a possible scenario.

If the queue went: GP…, the second would say: “I am G, previous was P”. This is a possible scenario.

If the queue went: PP…, the second would say: “I am G, previous was G”. This is not the case.

So the queue must alternate G’s and P’s.

The first man says that the final man questioned is P. So if the first is G the last is P, and if the first is P the last is G.

So the queue is one of: GP…GP, or: PG…PG. Either way is must be of even length. So there are equal numbers of G and P (say *n*).

If people are separated by an even number of others, then they are of different tribes, and if they are separated by an odd number of others then they must be from the same tribe.

If two people from the same tribe claim two different values for *n*, then they must both by lying and neither value can be correct:

The ugliest man says *n=19*. The man next but three will be the same tribe, and says *n=24*, so they must both be P, and both values are wrong.

The rudest says *n=24*. The next but but five will be the same tribe, and says *n=13*, so they must both be P, and both values are wrong.

So we know that *n ≠ 13, 19, 24*.

If two people from different tribes give different values for *n*, then one of them must be correct:

The fattest says *n=13*. The next but four will be of the other tribe, and says *n=28*. So one of these is correct.

So the only value that can be correct is *n=28*.

**Solution:** 28 of the men used *Phew*.

So we can say 28 of the men used *Phew* and 28 used *Grunt*, giving 56 in total. P and G users were questioned alternately.

The ugliest and the next but three were both P, as were rudest and the next but five. The fattest was also P and the next but four was G.

]]>The *autobiographical numbers* are also known as the *curious numbers*, hence the title of the puzzle.

I wrote some code to generate *autobiographical sequences*, which is a sequence where the element at index *n* counts the total number of occurrences of the element *n* in the entire sequence.

An autobiographical sequence represents an autobiographical number (in base 10), if all the elements have values that represent digits (i.e. are 0 – 9), and any element at an index higher than 9 is zero.

As 9 is the largest value we can have at element 0, and any elements beyond index 9 must be zero, so all decimal autobiographical numbers must have fewer than 20 digits.

This Python 3 program generates autobiographical sequences and totals the ones that correspond to decimal autobiographical numbers.

It runs in 739ms.

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

from enigma import irange, nconcat, arg, printf base = arg(10, 0, int) printf("[base = {base}]") # generate autobiographical sequences def _generate(a, m, s=[], k=0): if m < 0: return if s == a[:k]: yield s for i in irange(a[k], m): _s = s + [i] if i > k or a[i] != _s[i]: yield from _generate(a[:i] + [a[i] + 1] + a[i + 1:], m - i, _s, k + 1) # generate autobiographical sequences up to length n generate = lambda n: _generate([0] * (n + 1), n) # check a sequence is autobiographical def is_autobiographical(s): return all(x == s.count(i) for (i, x) in enumerate(s)) # record totals t = 0 for s in generate(2 * base - 1): assert is_autobiographical(s) if s and all(x < (base if i < base else 1) for (i, x) in enumerate(s)): n = nconcat(s, base=base) t += n printf("{s} -> {n}") printf("total = {t}")

**Solution:** The total of all such numbers is 10109876341430.

The full list of decimal autobiographical numbers is:

1210 2020 21200 3211000 42101000 521001000 6210001000 72100001000 821000001000 9210000001000 + -------------- 10109876341430

It turns out that the only autobiographical sequences with length less than 7 are:

()

(1, 2, 1, 0)

(2, 0, 2, 0)

(2, 1, 2, 0, 0)

and for *k ≥ 7* there is one autobiographical sequence of length *k*:

(k – 4, 2, 1, … , 1, …)

where the floating 1 is at index *k – 4* and the remaining elements are 0.

We can write a routine that uses this information to generate autobiographical sequences much faster than my original program.

See: [ OEIS A138480 ] [ https://en.wikipedia.org/wiki/Self-descriptive_number ]

]]>Suppose we use the following as our inductive hypothesis:

For all numbers between 5 and n there is a path between any pair of them using lines that link numbers between 5 and 1,000,000.

At *n=5* the hypothesis is trivially true.

Now want to show that the hypothesis holds for *n* (where *n < 250,000*) if it holds for all lower numbers.

If *n* is composite (say it can be factored into *a* and *b*), then we can link it directly to the number *(a + b)*, which is necessarily less than *n* and greater than or equal to 5.

However if *n* is prime we can’t directly link it to a lower number, but we can link it to higher number number of the form *2(n – 2), 3(n – 3), 4(n – 4), …*, and if one of these numbers is less than 1,000,000 and links to a number less than *n*, then there is a path that links *n* into the set of lower numbers.

This Python program looks for primes that do not have a viable path.

It runs in 767ms, and doesn’t find any numbers that cannot be linked in.

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

from enigma import irange, divisors_pairs, Primes, arg, number, printf N = number(arg("1_000_000", 0)) M = number(arg("250_000", 1)) printf("[N={N} M={M}]") # link x to a number less than n # return a shortest path def solve(x, n, N, s=[]): ss = [[x]] while ss: s = ss.pop(0) x = s[-1] # are we done? if x < n: return s # add in downward links for (a, b) in divisors_pairs(x): if a < 2: continue y = a + b if y < n: return s + [y] if y not in s: ss.append(s + [y]) # add in upward links for k in irange(2, x // 2): y = k * (x - k) if y > N: break if y not in s: ss.append(s + [y]) # attempt to link all primes between 6 and M - 1 for n in Primes(M).range(6): s = solve(n, n, N) if not s: printf("*** IMPOSSIBLE: {n} ***")

**Solution:** (1) No, there is no pair of numbers, both less than 250,000, that cannot be linked together (using numbers less than 1,000,000).

The first prime we encounter, 7, requires the most steps, as we only have a choice of two numbers to link it to (5 or 6):

7 → [3, 4] → 12 ← [2, 6] ← 8 ← [2, 4] ← 6

Most primes can be linked via *2(n – 2)* or *3(n – 3)*, for example:

353 → [2, 351] → 702 ← [3, 234] ← 237

And here is how we can link the largest primes less than 250,000, without including numbers more than 1,000,000:

249973 → [3, 249970] → 749910 ← [5, 149982] ← 149987

249989 → [2, 249987] → 499974 ← [3, 166658] ← 166661

If we reduce the ceiling from 1,000,000 to 749,900 then we find that 249,973 cannot be linked to a lower number.

% python enigma1035.py 749_900 [N=749900 M=250000] *** IMPOSSIBLE: 249973 ***

The smallest prime that needs to link via a number greater than (or equal to) 1,000,000 is:

333451 → [3, 333448] → 1000344 ← [4, 250086] ← 250090

So keeping the ceiling at 1,000,000 we can extend the result to:

There is no pair of numbers, both less than 333,451, that cannot be linked together (using numbers up to 1,000,000).

::

The second part of the puzzle deals with numbers between 5 and 100, and asks whether the links between these numbers form a “Planar Graph” [ https://en.wikipedia.org/wiki/Planar_graph ].

There are various algorithms for determining if a graph is planar, but they all sounded a bit complicated. I found a package called “planarity” [ https://pypi.org/project/planarity/ ] that implements one of the algorithms and used that.

The following Python program finds the links between the specified numbers, and then feeds the graph into the checker. Which produces a simple Yes/No answer.

from enigma import irange, arg, printf # valid numbers (from 5 to N) N = arg(100, 0, int) ns = set(irange(5, N)) # collect the links links = set() for n in ns: for k in irange(2, n // 2): m = k * (n - k) if m not in ns: continue printf("{n} -> {m}") links.add((n, m)) # check for planarity import planarity graph = planarity.PGraph(links) f = graph.is_planar() printf("is_planar = {f}")

**Solution:** (2) No, the lines cannot be drawn with no crossings.

We can demonstrate this as follows:

If instead of considering all the numbers between 5 and 100 (96 numbers) we just consider the following 13 numbers: (6, 7, 8, 9, 10, 11, 12, 14, 16, 18, 24, 28, 32), this is a subgraph of the original graph (every edge of the subgraph must appear in the original graph, so if the subgraph is non-planar the parent graph is also non-planar).

We can draw the graph as the following diagram:

This particular diagram is clearly non-planar, but we can show that it is not possible to draw a planar diagram for this graph.

We can remove the (10, 16) edge (and the graph is still a subgraph of the original graph), and the remaining edges can be laid out as follows:

From which we see that there is an embedded minor including (8, 18, 24) and (9, 11, 12) that forms the “utility graph” (K3,3), which is non-planar. [ https://en.wikipedia.org/wiki/Three_utilities_problem ]

It follows that the parent graph must also be non-planar (if it wasn’t it would give us a way to represent K3,3 using a planar diagram, and this is not possible).

I haven’t shown that this is the smallest non-planar subgraph, so there may be a smaller subgraph (with fewer than 13 elements) that has K3,3 embedded in it that can be used to show the non-planarity of the parent graph.

]]>This command line executes in 140ms.

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

% python -m enigma SubstitutedExpression "EBPNYE * E = YNHYAX" (EBPNYE * E = YNHYAX) (286752 * 2 = 573504) / A=0 B=8 E=2 H=3 N=7 P=6 X=4 Y=5 [1 solution]

**Solution:** The sum is: 286752 × 2 = 573504.