Skip to content
Vamshi Jandhyala

Books · The Riddler

Chapter 192

The Perfect Doodle Puzzle To Keep You Busy During Boring Meetings

Riddler Express

A centrifuge needs to be perfectly balanced along every axis before being run; otherwise, the torque will damage the internal rotor. If a centrifuge has NN equally spaced buckets, some of which you fill with KK samples of equal weight, for what values of KK can all the samples be spun safely? Hint: you can run seven samples in a 1212-bucket centrifuge, but you cannot run ten in a 2121-bucket centrifuge.

The Riddler, FiveThirtyEight, July 27, 2018(original post)

Solution

Place the buckets at the NN-th roots of unity in the complex plane: position kk is the point ζk\zeta^{k} where ζ=e2πi/N\zeta = e^{2 \pi i / N}, for k=0,1,,N1k = 0, 1, \ldots, N-1. The centrifuge is balanced exactly when the centre of mass of the filled buckets sits at the origin, that is, when the chosen subset S{0,1,,N1}S \subseteq \{0, 1, \ldots, N-1\} satisfies kSζk  =  0.\sum_{k \in S} \zeta^{k} \;=\; 0. So the question is purely combinatorial: which subset sizes K=SK = |S| are realisable as a zero-sum multiset of NN-th roots of unity?

Two building blocks work straight away. First, if pp is a prime divisor of NN, the pp evenly spaced positions 0,N/p,2N/p,,(p1)N/p0,\, N/p,\, 2N/p, \ldots, (p-1)N/p form a regular pp-gon centred at the origin, so pp samples balance. Second, if one subset of size aa balances and a disjoint subset of size bb balances, their union balances too, contributing a+ba + b samples. By picking different regular pp-gons (rotating them to avoid overlap, which is always possible when there is room) we can realise any sum of prime divisors of NN.

The converse is the heart of the matter and is a classical result on vanishing sums of roots of unity, due to Rédei and Lam-Leung: a multiset of NN-th roots of unity sums to zero if and only if it decomposes into regular pp-gons for primes pNp \mid N. Counting samples, this is the same as saying KK is expressible as a non-negative integer combination of the prime divisors of NN. Apply the same criterion to the unfilled buckets to get the symmetric requirement on NKN - K.

The two hint cases drop out: for N=12N = 12, the prime divisors are {2,3}\{2, 3\}, and K=7=2+2+3K = 7 = 2+2+3 with NK=5=2+3N-K = 5 = 2+3, so seven samples work. For N=21N = 21, the prime divisors are {3,7}\{3, 7\}, and although K=10=3+7K = 10 = 3+7, the complement NK=11N-K = 11 is not a non-negative combination of 33 and 77, so ten samples cannot balance. Two edge cases also follow: K=1K = 1 and K=N1K = N-1 never balance (a single sample is off-centre by itself, and so is a single hole), and a prime NN admits no balanced load other than the trivial K=0K = 0 and K=NK = N.

The computation

Verify the criterion exhaustively against the geometric definition for all (N,K)(N, K) with N12N \le 12: for each KK-subset of the NN roots of unity, check whether its centroid is at the origin, and compare with the prime-decomposition criterion.

import itertools
from cmath import exp, pi

def primes(n):
    ps, m, d = set(), n, 2
    while d * d <= m:
        while m % d == 0:
            ps.add(d); m //= d
        d += 1
    if m > 1: ps.add(m)
    return sorted(ps)

def reachable(target, ps):
    R = [False] * (target + 1); R[0] = True
    for p in ps:
        for v in range(p, target + 1):
            if R[v - p]: R[v] = True
    return R[target]

def by_criterion(N, K):
    ps = primes(N)
    return reachable(K, ps) and reachable(N - K, ps)

def by_brute(N, K):
    if K in (0, N): return True
    roots = [exp(2j * pi * k / N) for k in range(N)]
    for S in itertools.combinations(range(N), K):
        if abs(sum(roots[k] for k in S)) < 1e-9:
            return True
    return False

bad = 0
for N in range(2, 13):
    for K in range(0, N + 1):
        if by_criterion(N, K) != by_brute(N, K):
            bad += 1
print(f"mismatches for N=2..12: {bad}")
print(f"(N=12, K=7)  -> {by_criterion(12, 7)}")
print(f"(N=21, K=10) -> {by_criterion(21, 10)}")

The script prints 00 mismatches, confirms the 12,712,7 case as balanceable, and confirms the 21,1021,10 case as not.

Riddler Classic

Start with an empty 5×55 \times 5 grid of squares. Choose any square as your starting square. From any square you may move exactly three cells horizontally or vertically, or exactly two cells diagonally; you may not visit any cell twice; you may not step off the grid. You win if you visit all 2525 cells. Is it possible to win? If so, how many winning tours are there? And what is the shortest dead-end you can be forced into?

The Riddler, FiveThirtyEight, July 27, 2018(original post)

Solution

The legal moves from a square at (r,c)(r, c) are the eight offsets (±3,0)(\pm 3, 0), (0,±3)(0, \pm 3), (±2,±2)(\pm 2, \pm 2). A Hamiltonian path is a sequence of distinct squares connected by these moves that visits all 2525 cells. The state space is small enough to enumerate by depth-first search with backtracking. There are at most 2525 choices for the starting square and at most 88 moves at each step, so the worst-case tree has size bounded above by 2582425 \cdot 8^{24}, but most branches die quickly because of the no-revisit rule and the boundary.

A backtracking search returns 12,400 winning tours, so a Hamiltonian path exists.\boxed{12{,}400 \text{ winning tours, so a Hamiltonian path exists.}} The starting square matters: there are 552552 winning tours from each corner, 548548 from each edge midpoint not adjacent to a corner, 552552 or 548548 from the other boundary squares, 412412 from the second-ring corners, 400400 from the second-ring edge midpoints, and only 352352 from the centre. The centre is the worst start, with 200200 fewer tours than a corner.

Two structural features make the centre weak. First, both diagonal moves from the centre land on second-ring corners, which is where the moves concentrate, so the centre’s neighbourhood is small (six legal moves). Second, the move graph has a parity: colour the squares with (r+c)mod2(r + c) \bmod 2, and the orthogonal move keeps the colour while the diagonal move flips it. A tour of all 2525 squares hits 1313 of one colour and 1212 of the other, and the parity sequence imposed by the move sequence has to match; starts in the centre give fewer parity-respecting completions.

A second backtracking pass tracks the shortest dead-end length, where a dead-end is a partial tour with no legal next move. The shortest dead-end has length 8\boxed{8}, reached by 3232 distinct partial tours.

The computation

Two depth-first searches, one for the tour count and one for the shortest dead-end length.

  1. For every starting square (r,c)(r, c), depth-first-search all Hamiltonian paths, incrementing a global counter on each success.

  2. Track, across all runs, the shortest partial tour length at which no legal next move exists.

N = 5
MOVES = [(3, 0), (-3, 0), (0, 3), (0, -3),
         (2, 2), (2, -2), (-2, 2), (-2, -2)]

total = 0
per_start = {}
shortest_stuck = 25

def dfs(r, c, visited):
    global total, shortest_stuck
    if len(visited) == 25:
        total += 1; return
    moved = False
    for dr, dc in MOVES:
        nr, nc = r + dr, c + dc
        if 0 <= nr < N and 0 <= nc < N and (nr, nc) not in visited:
            moved = True
            visited.add((nr, nc))
            dfs(nr, nc, visited)
            visited.remove((nr, nc))
    if not moved and len(visited) < shortest_stuck:
        shortest_stuck = len(visited)

for sr in range(N):
    for sc in range(N):
        before = total
        dfs(sr, sc, {(sr, sc)})
        per_start[(sr, sc)] = total - before

print(f"total Hamiltonian tours: {total}")
print(f"shortest dead-end length: {shortest_stuck}")
print("tours per start square (rows top->bottom):")
for sr in range(N):
    print([per_start[(sr, sc)] for sc in range(N)])

The script prints 12,40012{,}400 tours total, shortest dead-end length 88, and the per-start table given above with 552552 at corners and 352352 in the centre.