Teaser 3130: Making squares
From The Sunday Times, 18th September 2022 [link] [link]
Liam has nine identical dice. Each die has the usual numbers of spots from 1 to 6 on the faces, with the numbers of spots on opposite faces adding to 7. He sits at a table and places the dice in a 3×3 square block arrangement.
As I walk round the table I see that (converting numbers of spots to digits) each vertical face forms a different three-figure square number without a repeating digit.
As Liam looks down he sees six three-digit numbers (reading left to right and top to bottom) formed by the top face of the block, three of which are squares. The total of the six numbers is less than 2000.
What is that total?
[teaser3130]
Jim Randell 5:29 pm on 16 September 2022 Permalink |
At first I found multiple solutions. But you can find a unique answer to the puzzle if you ensure the dice really are identical.
This Python program runs in 261ms.
Run: [ @replit ]
from enigma import (powers, nsplit, subsets, seq_all_same_r, nconcat, irange, printf) # some useful routines for checking dice ... # value on opposite face # 0 1 2 3 4 5 6 opp = [0, 6, 5, 4, 3, 2, 1] # valid edge? edge = lambda x, y: x != y and x != opp[y] # construct (x, y) -> z corner maps for a right handed die def corners(x, y, z): d = dict() for (a, b, c) in [(x, y, z), (x, z, opp[y]), (x, opp[y], opp[z]), (x, opp[z], y)]: for (p, q, r) in [(a, b, c), (b, c, a), (c, a, b)]: d[(p, q)] = r d[(opp[p], opp[r])] = opp[q] return d # valid corner? # -> 0 if invalid; 1 if right-handed; -1 if left-handed def corner(x, y, z, d=corners(1, 2, 3)): r = d.get((x, y), 0) if r == z: return 1 if r == opp[z]: return -1 return 0 # now on with the puzzle ... # possible 3-digit squares (as numbers) / vertical squares (as digits) (sqs, sqvs) = (set(), set()) for n in powers(10, 31, 2): ds = nsplit(n) if min(ds) < 1 or max(ds) > 6: continue sqs.add(n) if len(set(ds)) == 3: sqvs.add(ds) # consider the layout: # # W V U # +-----+ # K |A B C| T # L |D E F| S # M |G H I| R # +-----+ # N P Q # choose the squares around the edges (all different) for ((K, L, M), (N, P, Q), (R, S, T), (U, V, W)) in subsets(sqvs, size=4, select="P"): # choose the top faces for the corner dice for (A, C, G, I) in subsets(irange(1, 6), size=4, select="M"): corners = [(A, W, K), (G, M, N), (I, Q, R), (C, T, U)] r = seq_all_same_r(corner(*vs) for vs in corners) if not (r.same and r.value != 0): continue # choose the remaining top faces for (B, D, E, F, H) in subsets(irange(1, 6), size=5, select="M"): edges = [(D, L), (H, P), (F, S), (B, V)] if not all(edge(*vs) for vs in edges): continue # construct the 6 top numbers numbers = [(A, B, C), (D, E, F), (G, H, I), (A, D, G), (B, E, H), (C, F, I)] ns = list(nconcat(*vs) for vs in numbers) # the sum is less than 2000 t = sum(ns) if not (t < 2000): continue # three of them are squares if sum(n in sqs for n in ns) != 3: continue # output solution printf("{t} <- {hs} {vs}; [{K}{L}{M}, {N}{P}{Q}, {R}{S}{T}, {U}{V}{W}]", hs=ns[:3], vs=ns[3:])(or a faster variation on [@replit]).
Solution: The total is 1804.
The dice are laid out as follows:
So the total is: (115 + 441 + 445) + (144 + 144 + 515) = 1804.
Of the six numbers read off from the top of the arrangement the square numbers are: 441 (= 21²) and 144 (twice; = 12²).
Note that each of the corner dice is left-handed (i.e. a mirror image of a “standard” die), and so, as the dice are all identical, they must all be left-handed.
If we are allowed to mix left- and right-handed dice, then there are many possible layouts (and many possible answers to the puzzle).
LikeLike
Frits 9:50 pm on 16 September 2022 Permalink |
Thanks to Jim, hopefully with the correct solution.
from itertools import permutations, product from functools import reduce # convert digits to number d2n = lambda s: reduce(lambda x, y: 10 * x + y, s) # 3-digit squares using digits 1-6 sqs = [s for x in range(10, 27) if not any(y in (s := str(x * x)) for y in "7890")] # 3-digit squares with different digits side_sqs = [int(s) for s in sqs if len(set(s)) == 3] sqs = set(int(s) for s in sqs) # +-------+ so bottom = 4, left = 5, back = 6 # / 3 /. # / /2 # +-------+ . # | 1 | # | |. # +-------+ # we have nine identical dice # so with two clockwise adjacent faces, say (2, 3), # the upper face is fixed (I have set it to 6) d = dict() # set the number facing up for clockwise pair (a, b) d[(1, 2)] = 4 d[(1, 3)] = 2 d[(2, 3)] = 6 d[(2, 6)] = 4 d[(3, 5)] = 6 d[(3, 6)] = 2 d[(4, 5)] = 1 d[(4, 6)] = 5 d[(5, 6)] = 3 # add decreasing pairs for k, v in d.copy().items(): d[k[::-1]] = 7 - v for p in permutations(side_sqs, 4): edges = [] corners = [] # calculate candidate numbers facing up for each corner and each edge for x, y in zip(p, p[1:] + (p[0], )): # connect squares x and y corner_sides = (x % 10, y // 100) if corner_sides not in d: break corners.append([d[corner_sides]]) edges.append([i for i in range(1, 7) if i not in {(x % 100) // 10, 7 - ((x % 100) // 10)}]) # each corner should have only one candidate if len(corners) != 4: continue edges = edges[1:] + [edges[0]] # 3x3 block of dice block = [corners[2], edges[1], corners[1], edges[2], range(1, 7), edges[0], corners[3], edges[3], corners[0]] for p2 in product(*block): # six three-digit numbers on top face F = [d2n([p2[3 * i + j] for j in range(3)]) for i in range(3)] + \ [d2n([p2[3 * j + i] for j in range(3)]) for i in range(3)] # the total of the six numbers is less than 2000 ... if sum(F) >= 2000: continue # ... three of which are squares . if sum([f in sqs for f in F]) != 3: continue print(" ", str(p[2])[::-1]) print(str(p[3])[0], p2[:3], str(p[1])[2]) print(str(p[3])[1], p2[3:6], str(p[1])[1]) print(str(p[3])[2], p2[6:], str(p[1])[0]) print(" ", p[0]) print() print("total:", sum(F))LikeLike
Frits 10:03 pm on 16 September 2022 Permalink |
The side squares should be seen when walking anti-clockwise around the table (so the top and right squares are printed reversed).
LikeLike
Hugh Casement 7:17 am on 17 September 2022 Permalink |
Are they left-handed dice? I can’t find any solution with standard dice (such as Frits shows).
Or perhaps you have to walk round the table with your head upside down.
LikeLike
Jim Randell 7:25 am on 17 September 2022 Permalink |
@Hugh: Yes. There is only a solution if left-handed dice are used (at least in the corners of the layout – and as the dice are identical then the rest must be left-handed too).
LikeLike
Frits 9:40 am on 17 September 2022 Permalink |
I set up my dice right-handed (I didn’t even know the term right-handed) based on numbers facing up when going clock wise. However, the corner arrangements in the block have to be read anti-clockwise so I should have used the 7-complement of my hardcoded values.
My solution is the same as Jim’s and is left-handed. I will post a new version checking both left-handed and right-handed dice (using Brian Gladman’s function for determining the hardcoded values).
LikeLike
Frits 1:52 pm on 17 September 2022 Permalink |
Supporting right-hand and left-hand dice and with a recursive version of Brian Gladman’s function for third face values.
from itertools import permutations, product from functools import reduce # find the third face anti-clockwise around a dice vertex # given two faces in anti-clockwise order def f3(fi, se, rh=True): # invalid first/second combination if fi == se or fi + se == 7: return 0 # only calculate for 3 low increasing pairs if (fi, se) in {(1, 2), (1, 3), (2, 3)}: c = 0 if rh else 7 # complement related return abs(c - (fi * (2 * se - 1)) % 9) # artificial formula # as of now work towards low increasing pairs if se < fi: return 7 - f3(se, fi, rh) elif fi > 3 and se > 3: return f3(7 - fi, 7 - se, rh) # double complement elif fi > 3: return 7 - f3(7 - fi, se, rh) elif se > 3: return 7 - f3(fi, 7 - se, rh) # convert digits to number d2n = lambda s: reduce(lambda x, y: 10 * x + y, s) # 3-digit squares using digits 1-6 sqs = [s for x in range(10, 27) if not any(y in (s := str(x * x)) for y in "7890")] # 3-digit squares with different digits side_sqs = [int(s) for s in sqs if len(set(s)) == 3] sqs = set(int(s) for s in sqs) for p in permutations(side_sqs, 4): # check both right-handed and left-handed dice for rh in [1, 0]: edges = [] corners = [] # calculate candidate numbers facing up for each corner and each edge for x, y in zip(p, p[1:] + (p[0], )): # connect squares x and y and calculate top face top = f3(x % 10, y // 100, rh) if not top: break corners.append([top]) edges.append([i for i in range(1, 7) if i not in {(x % 100) // 10, 7 - ((x % 100) // 10)}]) else: edges = edges[1:] + [edges[0]] # rearrange # 3x3 block of dice block = [corners[2], edges[1], corners[1], edges[2], range(1, 7), edges[0], corners[3], edges[3], corners[0]] for p2 in product(*block): # six three-digit numbers on top face F = [d2n([p2[3 * i + j] for j in range(3)]) for i in range(3)] + \ [d2n([p2[3 * j + i] for j in range(3)]) for i in range(3)] # the total of the six numbers is less than 2000 ... if sum(F) >= 2000: continue # ... three of which are squares if sum([f in sqs for f in F]) != 3: continue print(" ", str(p[2])[::-1]) print(str(p[3])[0], p2[:3], str(p[1])[2]) print(str(p[3])[1], p2[3:6], str(p[1])[1]) print(str(p[3])[2], p2[6:], str(p[1])[0]) print(" ", p[0], "\n") print("total:", sum(F), "(right-handed" if rh else "(left-handed", "dice)")LikeLike
Frits 11:39 pm on 17 September 2022 Permalink |
Based on Jim’s first posted program. This program runs in 90ms.
from enigma import SubstitutedExpression # consider the layout: # # W V U # +-----+ # K |A B C| T # L |D E F| S # M |G H I| R # +-----+ # N P Q # corners for a right-handed die die = {(1, 2, 3), (1, 3, 5), (1, 5, 4), (1, 4, 2), (6, 4, 5), (6, 5, 3), (6, 3, 2), (6, 2, 4)} # check faces anti-clockwise around a corner (= x, y, z) # 1 = right-handed, -1 = left-handed def corner(x, y, z): ss = {x, y, z} return (1 if die.intersection({(x, y, z), (y, z, x), (z, x, y)}) else -1) # the alphametic puzzle p = SubstitutedExpression( [ "W != 7 - K", "is_square(KLM)", "U != 7 - T", "is_square(UVW)", "R != 7 - Q", "is_square(RST)", "N != 7 - M", "is_square(NPQ)", "seq_all_different([KLM, NPQ, RST, UVW])", # corner checks "A not in {7 - W, K, 7 - K}", "C not in {7 - U, T, 7 - T}", "I not in {7 - R, Q, 7 - Q}", "G not in {7 - M, N, 7 - N}", # edge checks "D != 7 - L", "B != 7 - V", "F != 7 - S", "H != 7 - P", "seq_all_same(corner(*vs) for vs in [(A, W, K), (G, M, N), \ (I, Q, R), (C, T, U)])", "sum([ABC, DEF, GHI, ADG, BEH, CFI]) < 2000", "sum([1 for x in [ABC, DEF, GHI, ADG, BEH, CFI] if is_square(x)]) == 3", ], answer="(ABC, DEF, GHI), (KLM, NPQ, RST, UVW), \ sum([ABC, DEF, GHI, ADG, BEH, CFI])", distinct=("KLM","NPQ","RST","WVU","AWK","UCT","IRQ","MGN", \ "DL","BV","FL","HP"), env=dict(corner=corner), digits=range(1, 7), reorder=0, denest=32, # [CPython] work around statically nested block limit verbose=0, # use 256 to see the generated code ) # print answers for (_, ans) in p.solve(): print(" ", str(ans[1][3])[::-1]) print(f"{str(ans[1][0])[0]} {ans[0][0]} {str(ans[1][2])[2]}") print(f"{str(ans[1][0])[1]} {ans[0][1]} {str(ans[1][2])[1]}") print(f"{str(ans[1][0])[2]} {ans[0][2]} {str(ans[1][2])[0]}") print(" ", ans[1][1], " sum", ans[2], "\n")LikeLike
Frits 11:02 pm on 22 September 2022 Permalink |
More efficient version.
from itertools import permutations, product from functools import reduce from collections import defaultdict # find the third face anti-clockwise around a dice vertex # given two faces in anti-clockwise order def f3(fi, se, rh=True): # invalid first/second combination if fi == se or fi + se == 7: return 0 # only hardcode for 3 low increasing pairs match (fi, se): case (1, 2): return 3 if rh else 4 case (1, 3): return 5 if rh else 2 case (2, 3): return 1 if rh else 6 case _: # work towards low increasing pairs if se < fi: return 7 - f3(se, fi, rh) elif fi > 3 and se > 3: return f3(7 - fi, 7 - se, rh) elif fi > 3: return 7 - f3(7 - fi, se, rh) elif se > 3: return 7 - f3(fi, 7 - se, rh) # convert digits to number d2n = lambda s: reduce(lambda x, y: 10 * x + y, s) # 3-digit squares using digits 1-6 sqs = [s for x in range(10, 27) if not any(y in (s := str(x * x)) for y in "7890")] # map hundreds and units digits to tens digits in squares hu2ts = defaultdict(set) for s in sqs: h, t, u = (int(d) for d in s) hu2ts[h, u].add(t) # 3-digit squares with different digits side_sqs = [int(s) for s in sqs if len(set(s)) == 3] sqs = set(int(s) for s in sqs) for p in permutations(side_sqs, 4): # check both right-handed and left-handed dice for rh in [1, 0]: edges = [] corners = [] # calculate candidate numbers facing up for each corner and each edge for x, y in zip(p, p[1:] + (p[0], )): # connect squares x and y and calculate top face top = f3(x % 10, y // 100, rh) if not top: break corners.append(top) edges.append([i for i in range(1, 7) if i not in {(x % 100) // 10, 7 - ((x % 100) // 10)}]) else: edges = edges[1:] + [edges[0]] # rearrange # c[2] e[1] c[1] # e[2] . e[0] # c[3] e[3] c[0] # skip if sum of the four top numbers that don't use the center # is already too high if 100 * (2 * corners[2] + corners[1] + corners[3]) + 4 * 10 + \ corners[3] + 2 + corners[0] + corners[1] + 222 > 1999: continue # which of these four top numbers can make a square number ts = [] for i, (a, b) in enumerate([(1, 0), (2, 1), (2, 3), (3, 0)]): # can we form a square with 2 corners? if len(hu2ts[corners[a], corners[b]]): # which edge candidates result in a square cands = hu2ts[corners[a], corners[b]] & set(edges[i]) if cands: ts.append((i, cands)) if not ts: continue # we can't make a square elif len(ts) == 1: # only one top number can make a square, reduce the edge candidates edges[ts[0][0]] = list(ts[0][1]) for e4 in product(*edges): # four three-digit numbers on top face (without the center) T4 = [d2n([corners[1], e4[0], corners[0]]), d2n([corners[2], e4[1], corners[1]]), d2n([corners[2], e4[2], corners[3]]), d2n([corners[3], e4[3], corners[0]])] if sum(T4) + 222 > 1999: continue # center die for c in range(1, 7): # add two missing top numbers T6 = T4 + [d2n([e4[1], c, e4[3]]), d2n([e4[2], c, e4[0]])] # the total of the six numbers is less than 2000 ... if sum(T6) >= 2000: continue # ... three of which are squares if sum([t in sqs for t in T6]) != 3: continue print(" ", str(p[2])[::-1]) print(str(p[3])[0], T6[1], str(p[1])[2]) print(str(p[3])[1], T6[3], str(p[1])[1]) print(str(p[3])[2], T6[5], str(p[1])[0]) print(" ", p[0], end=" ") print("total:", sum(T6), "(right-handed" if rh else "(left-handed", "dice)")LikeLike