Chapter 227
How Many Soldiers Do You Need To Beat The Night King?
Riddler Express
Navigate a nine-by-nine grid of colored lines three times, once as blue, once as yellow, once as red, traversing only lines containing your color. The maze graphic is supplied in the original post.
The Riddler, FiveThirtyEight, May 17, 2019(original post)
Status
This puzzle’s statement is the maze graphic itself: a nine-by-nine grid in which each edge has a colour drawn from blue, yellow, red, green, purple, orange, white (passable by every colour), or black (passable by none), with the start and finish vertices marked on the image. The corpus does not preserve the maze as machine-readable data, and the puzzle is unsolvable without the per-edge colour assignment.
The official solution shipped the route as another image. With the input image and the route image both lost to text-only archives, this puzzle is deferred for the same reason as earlier image-only Riddlers, the gerrymander grid of October and the map-colouring of November .
Riddler Classic
The Night King raises every fallen living soldier into the army of the dead. Each army lines up single file. One soldier steps forward from each side and they duel; each side wins with probability . If the living wins, the dead soldier is gone forever and the living goes to the back of his line. If the dead wins, the dead goes to the back of his line and the (formerly) living soldier joins him there. The battle continues until one army is empty. What starting sizes (living) and (dead) give each army a – chance of winning?
The Riddler, FiveThirtyEight, May 17, 2019(original post)
Solution
Let be the probability that the living army wins, starting from living and dead. Boundary cases: At any interior state the next duel resolves with probability each way, giving the recursion The puzzle asks for with .
Random-walk view. Index duels by and let be the outcome of the -th duel from the living’s perspective: if the living wins, if the dead wins. The are i.i.d. fair coin flips. After wins and losses by the living, The living army wins iff the dead reach before the living do, i.e., iff Equivalently, hits before the number of steps reaches .
Why the scaling is . Each duel costs the dead exactly one soldier in expectation: with probability the dead lose one, and with probability they gain one, so the dead random walk has zero drift and standard deviation per step. The dead army’s count is a fair random walk that absorbs at from the start point . By the gambler’s-ruin result on a fair walk, the expected number of duels until the dead count touches is infinite, but the median is on the order of .
The living army, in contrast, loses exactly one soldier with probability per duel and is unchanged otherwise, so its count is a one-sided walk with drift . After duels the living is expected to have lost soldiers. For the living to outlast the dead’s exhaustion (median time ), the living needs . The factor in front turns out to be close to for small and grows slowly: numerical solution of the recursion gives the half-chance frontier in the table below.
Half-chance frontier (smallest with ). The ratio rises gently from at to about at , consistent with the asymptotic derived above.
The headline number from the official is for as a rule-of-thumb (square scaling), but the exact frontier rounds slightly above for . The discrepancy comes from the asymmetry of the duel: each living loss adds to the dead, so the dead’s random walk drifts upward whenever the living lose, and the living need a bit more than to compensate.
The computation
Encode the duel directly: track pair, draw fair coin flips, and record who wins. The empirical win rate at the boxed pairs must match . Cross-check against the DP that solves the recursion exactly.
Implement the recursion with memoisation. The state space at is bounded by total budget duels, which the DP iterates in ascending budget order.
For each from to , binary-search the smallest with .
Monte Carlo the duel sequence at a few boxed pairs.
import random
def solve(target_D, target_L):
Dmax = target_D + target_L + 2
Lmax = target_L + 2
p = [[0.0] * (Dmax + 1) for _ in range(Lmax + 1)]
for L in range(Lmax + 1):
p[L][0] = 1.0 if L >= 1 else 0.0
for tot in range(1, 2 * Lmax + Dmax + 1):
for L in range(0, Lmax + 1):
D = tot - 2 * L
if D <= 0 or D > Dmax:
continue
if L == 0:
p[0][D] = 0.0
continue
nxt = p[L - 1][D + 1] if D + 1 <= Dmax else 0.0
p[L][D] = 0.5 * p[L][D - 1] + 0.5 * nxt
return p[target_L][target_D]
def smallest_L(D, hi=2000):
lo = 1
while lo < hi:
mid = (lo + hi) // 2
if solve(D, mid) >= 0.5:
hi = mid
else:
lo = mid + 1
return lo
def simulate(L, D, trials):
wins = 0
for _ in range(trials):
Ll, Dd = L, D
while Ll > 0 and Dd > 0:
if random.random() < 0.5:
Dd -= 1 # living wins duel
else:
Ll -= 1
Dd += 1 # dead wins duel
if Dd == 0:
wins += 1
return wins / trials
random.seed(2019)
for D in [1, 2, 3, 4, 5, 6, 8, 10, 15, 20]:
L = smallest_L(D)
p_exact = solve(D, L)
p_sim = simulate(L, D, 20_000) if D <= 10 else float("nan")
print(f"D={D:>3}: L={L:>4} p_exact={p_exact:.4f} "
f"L/D^2={L/D**2:.3f} p_sim={p_sim:.4f}")
The script prints the half-chance frontier matching the boxed table, with the empirical Monte Carlo agreeing to within at each pair where simulation is run.