Chapter 206
The Riddler Just Had To Go And Reinvent Beer Pong
Riddler Express
Two grandmasters play a World Chess Championship match. The better player wins each game with probability , loses with probability , and draws with probability . Wins count point, draws each, losses . The match is games; the first to points wins the match outright (ties don’t decide the title here). What are the chances the better player wins a -game match? How many games does the match need to give the better player a , , or chance of winning outright?
The Riddler, FiveThirtyEight, November 16, 2018(original post)
Solution
A game contributes , , or to the margin (better player’s score minus opponent’s, in units of half points doubled). The better player wins the match outright when the margin after games is strictly positive. The match outcome is the sign of with and probabilities
Generating function. Encode each game by the Laurent polynomial . The match polynomial is ; the coefficient of is the probability the margin equals . The match-win probability is the sum of coefficients on the positive powers of : This is a clean polynomial computation: expand and sum the positive-exponent coefficients.
. Direct expansion gives For the thresholds, scan upwards and stop at the first exceeding each target:
Sanity check. The expected margin per game is , and the variance is . By the central limit theorem, the margin after games is approximately Gaussian with mean and standard deviation , and the match-win probability is For this is (overshoots a little, as the Gaussian approximation does for a discrete margin near zero). For , solving gives ; the true value exceeds it by about , since for moderate the discreteness of the margin (mass at , then jumps by ) and the long tail of draws sit far enough from the asymptotic Gaussian shape that the threshold integer is meaningfully larger.
The computation
Build by iterated polynomial multiplication and sum the coefficients on the positive exponents. Use Python integers and rationals to avoid floating-point drift over large .
Represent the running polynomial as a dictionary .
Multiply by each game: shift mass by with weight , leave with , shift by with .
Sum positive-exponent mass for the answer.
from collections import defaultdict
def p_win(N):
coef = {0: 1.0}
for _ in range(N):
nxt = defaultdict(float)
for r, c in coef.items():
nxt[r + 1] += 0.20 * c
nxt[r] += 0.65 * c
nxt[r - 1] += 0.15 * c
coef = nxt
return sum(c for r, c in coef.items() if r > 0)
print(f"N = 12: P(win) = {p_win(12):.4f}")
for target in (0.75, 0.90, 0.99):
N = 1
while p_win(N) < target:
N += 1
print(f"target {target:.2f}: smallest N = {N}")
The script prints for and the thresholds , , , matching the boxed answer.
Riddler Classic
The game has cups labelled through and an infinite supply of balls each labelled through (uniformly random per ball). Each round has two phases. In the throwing phase the player takes balls one at a time from the supply and throws them; each throw lands in a uniformly random cup; the phase ends when every cup contains at least one ball. In the pruning phase the player goes through each cup and removes every ball whose number does not match the cup’s label. The game ends after a round in which no cup is empty after the pruning. How many rounds is the game expected to take? How many throws?
The Riddler, FiveThirtyEight, November 16, 2018(original post)
Solution
After each round, every cup ends either locked (holding at least one ball whose label matches the cup, kept by pruning forever) or open (empty after pruning). Locked cups remain locked since their kept balls satisfy the no-empty-cup termination criterion for free in subsequent rounds. Let be the number of locked cups at the end of round . The game ends at the first with .
Throws per round. Suppose round starts with locked cups. Locked cups already have a matching ball, so the throwing phase already has them in the "non-empty" state. The phase continues until each of the remaining open cups receives at least one ball. Each throw goes uniformly to one of cups, so the round’s throw count is the cover time for the open cups: The largest term, when only one open cup remains, contributes throws by itself. The sum grows like for .
Locking new cups. Each open cup that receives at least one ball this round has independent probability that the ball matches the cup label, and the cup might receive several balls (each independently matching with probability ). Let be the number of balls received by open cup in this round; the cup locks iff at least one of those balls matches, with probability given .
The exact joint distribution of given throws is the multinomial restricted to all positive counts. A clean closed form is not at hand, but the round dynamics are easy to simulate, and the asymptotic structure is clear:
The number of rounds grows roughly linearly with : each round locks an expected new cups on average among the open ones, since open cup receives balls in a typical round when many cups are still open, each matching independently with probability , so the match probability per round is . Multiplied by open cups, the expected new locks per round is , and rounds.
The number of throws grows roughly quadratically with : each round costs throws (cover time on the open cups), and there are rounds, for total.
For concrete numbers, a simulation gives and at ; rounds and throws at . The ratio is roughly at and at , consistent with the scaling.
The computation
Encode the round dynamics exactly and run trials per . Confirm rounds scale roughly linearly and throws roughly quadratically.
Maintain a Boolean vector
locked[1..N].Per round: draw labelled balls into uniform cups until every cup has at least one ball (locked or new).
For each new ball in cup , if its label equals set
locked[c]to true.Stop at the first round where all entries are locked.
import random
from statistics import mean
def play(N, rng):
locked = [False] * N
rounds = 0
throws = 0
while not all(locked):
rounds += 1
touched = list(locked) # locked cups count as non-empty
new_balls = {i: [] for i in range(N) if not locked[i]}
while not all(touched):
label = rng.randrange(N)
cup = rng.randrange(N)
throws += 1
touched[cup] = True
if not locked[cup]:
new_balls[cup].append(label)
for cup, balls in new_balls.items():
if any(b == cup for b in balls):
locked[cup] = True
return rounds, throws
rng = random.Random(0)
for N in (3, 5, 7, 10):
samples = [play(N, rng) for _ in range(10_000)]
er = mean(r for r, _ in samples)
et = mean(t for _, t in samples)
print(f"N={N}: E[rounds]~{er:.2f}, E[throws]~{et:.2f}, "
f"throws/N^2~{et / N**2:.2f}")
The script confirms that the rounds count grows linearly and the throws count grows quadratically, with the constants slowly creeping up due to the factor in the cover-time term.
Extra Credit
It’s been a while: time for a Coolest Riddler Extension Award. What if the game weren’t so simple? What if you could aim? What if you could miss? The most interesting twist on this game earns a shiny emoji trophy.
Solution
This is a participatory contest call (Coolest Riddler Extension), not a derivable mathematical question. Like the other community-submission Extras (the trap-effectiveness Express in chapter being the closest example), it is deferred.