Chapter 210
Santa Needs Some Help With Math
Riddler Express
Santa has to harness reindeer in a specific (but to him forgotten) order. Each reindeer knows its position and will grunt if placed in the right slot. Santa lists the reindeer in a random order and follows this strategy: at the first slot he tries reindeer one by one from the top of his list until one grunts; once it does, he leaves it there and goes to the second slot, again starting from the top of the (now shorter) list. Each harness attempt takes one minute. How many minutes does it take on average to harness all eight? Extra credit: is there a strategy that does better?
The Riddler, FiveThirtyEight, December 21, 2018(original post)
Solution
Number the slots with . Because Santa’s list is uniformly random, when he arrives at slot the remaining reindeer are in a uniformly random order, and the correct one for slot is uniformly placed among them. The number of attempts to find that grunt is uniform on , with expectation By linearity, the expected total time across all slots is For :
Extra credit (a better strategy?). No. The information Santa gains from a failed attempt at slot is that the reindeer he just tried is not the slot- reindeer; he already learned which reindeer is the slot- reindeer the moment one grunted. That information does not constrain which reindeer is the slot- reindeer for . Conditional on Santa having placed the first reindeer correctly, the remaining reindeer are a uniformly random permutation over the remaining slots, regardless of the order in which he tried earlier ones. So no reordering of his subsequent attempts changes the expected cost at any slot, and the expected total stays at .
This conclusion is sharp under the uniform-prior model. Different models (for example, Santa choosing his initial list non-uniformly, or some reindeer being more memorable than others) might shift the answer; under the puzzle as stated, minutes is the best Santa can do on average.
The computation
Simulate Santa’s strategy directly and compare against the closed form .
Sample a uniformly random permutation as Santa’s initial list.
For each slot in order, scan the remaining reindeer until the correct one grunts.
Record total attempts and average across many trials.
import random
from statistics import mean
def santa(N, trials=200_000, seed=0):
rng = random.Random(seed)
totals = []
for _ in range(trials):
perm = list(range(N))
rng.shuffle(perm)
remaining = perm[:]
cost = 0
for slot in range(N):
for k, r in enumerate(remaining):
cost += 1
if r == slot:
remaining.pop(k)
break
totals.append(cost)
return mean(totals)
for N in (3, 5, 8, 10):
closed = N * (N + 3) / 4
print(f"N={N}: simulated={santa(N):.3f}, closed={closed:.3f}")
The script prints simulated averages within of the closed-form for each , with the headline giving minutes exactly.
Riddler Classic
During one shift in Santa’s workshop, elves hear songs chosen uniformly at random with replacement from a playlist of songs. The cranky elf throws snowballs whenever the same song plays twice in a shift; this has happened during about half of the shifts. How large is the playlist?
The Riddler, FiveThirtyEight, December 21, 2018(original post)
Solution
This is the birthday problem with playlist size replacing and shift length replacing party size .
Setup. The probability that a -song shift has no repeat is The probability of at least one repeat is . Cranky throws snowballs about half the time, so , i.e.
Solving for . The function is strictly increasing in (more songs means more spread), so a unique (up to integer rounding) gives . Apply : The leading-order Taylor expansion gives so at . The constant-correction from the next Taylor term lifts the answer slightly; a numerical search for the integer crossing gives At , ; at , .
The computation
Compute in floats and binary-search for the integer at which crosses .
For each candidate , compute by summing logarithms.
Binary-search over for the crossing of .
Confirm by evaluating at the answer and the answer minus one.
import math
def Q(X, n=100):
return math.exp(sum(math.log((X - k) / X)
for k in range(n)))
lo, hi = 100, 100_000
while hi - lo > 1:
mid = (lo + hi) // 2
if Q(mid) < 0.5:
lo = mid
else:
hi = mid
print(f"X = {hi}: Q(X) = {Q(hi):.6f}")
print(f"X = {lo}: Q(X) = {Q(lo):.6f}")
print(f"Crossing X = {hi}")
The script prints with and , matching the boxed answer.