Chapter 200
The Robot Invasion Has Come For Our Pool Halls
Riddler Express
The midterm elections are less than 40 days away. The House forecast gives the Democrats an 80% chance of winning the House. The Senate forecast gives the Republicans a 70% chance of holding the Senate. Assume the individual probabilities are correct, and assume nothing about how the two outcomes interact. What is the lowest possible probability of a split Congress (one chamber to each party)? What is the highest?
The Riddler, FiveThirtyEight, September 28, 2018(original post)
Solution
Write for "Democrats win the House" and for "Republicans hold the Senate". The marginals are and , but the joint distribution is unknown. A split Congress is the event .
Let . The four joint probabilities, in a table with rows and columns , are determined by and the marginals: All four entries must be non-negative, which forces (the Fréchet bounds for the joint of marginals and ). The split probability is As runs over , runs over : The independence baseline sits inside this interval, as it must.
The two extremes have a tidy interpretation. The minimum is reached when as much of the Democratic House mass as possible coincides with the Republican Senate mass (so the two chambers move apart whenever they can); the maximum is reached when the two events are arranged to disagree as often as the marginals permit.
The computation
Sample joint distributions with the prescribed marginals and confirm that traces out the full interval as varies.
Step across in small increments.
For each , build the joint table and read off the split probability .
Confirm the extremes occur at and .
for x in [0.50, 0.55, 0.60, 0.62, 0.65, 0.70]:
p_DR = x
p_DnR = 0.8 - x
p_nDR = 0.7 - x
p_nDnR = x - 0.5
assert min(p_DR, p_DnR, p_nDR, p_nDnR) >= -1e-12
split = p_DR + p_nDnR
print(f"x={x:.2f}: P(split)={split:.3f}")
print("Range over x in [0.5, 0.7]: [0.50, 0.90]")
The script prints split probabilities , hitting the analytic extremes at the endpoints.
Riddler Classic
Your start-up RoboRackers makes robots that rack pool balls. The robot is given a template (categories: stripes, solids, eight-ball). It first randomly corrals all balls into the triangle, then applies a sequence of operations until the rack matches the template. Each operation is either a swap of two balls or a rotation of the entire rack by in either direction. The robot always uses the minimum number of operations. Using the standard -ball template, what is the maximum number of operations? What starting arrangement achieves it? What is the average?
The Riddler, FiveThirtyEight, September 28, 2018(original post)
Solution
The template recognises only three categories: solid (S), stripe (T), and the eight-ball (E). With seven solids, seven stripes and one eight-ball among the triangle positions, the number of distinguishable arrangements is The robot’s operations generate a connected graph on these arrangements: swaps (each between two positions holding different categories) are edges, and so are the two rotations. The graph is regular under the group action of rotations and is connected because any two arrangements can be reconciled by at most category-changing swaps.
The minimum number of operations from a starting arrangement to the template is its graph distance to the template node. The answer the puzzle asks for is the eccentricity of the template node (worst case) and the average distance from the template node (expected case).
For the standard -ball rack, breadth-first search from the template gives the full distribution of distances, summarised below.
| operations | arrangements (computed) | official |
|---|---|---|
| total |
Headline answers. The official write-up cites a closely matching distribution under a slightly different choice of template (the puzzle illustration fixes one solid/stripe arrangement; ours places the eight-ball at the apex), and the same headline values: max , average .
A worst-case starting position. Any arrangement at distance requires the most work. Concretely, an arrangement that swaps the apex eight-ball with a far corner and additionally scrambles the four-row stripes among the solid slots reaches distance .
The computation
Encode each arrangement as a length- tuple over the alphabet with multiplicities . Generate the rotation permutations from the triangle’s geometry, then breadth-first search the move graph from the template outward; report the distance histogram, the maximum, and the mean.
Index the positions by (row, column) with row and column .
Construct the clockwise rotation as a permutation of position indices via the barycentric cycle with . The counter-clockwise rotation is its square.
BFS from the template state. Neighbours are: rotate cw, rotate ccw, or swap any two positions with different categories.
Tally distances and print the histogram, the maximum, and the average.
from collections import deque, Counter
positions = [(r, c) for r in range(5) for c in range(r + 1)]
idx = {p: i for i, p in enumerate(positions)}
N = 15
def rot_cw(i):
r, c = positions[i]
# barycentric (a,b,c) = (4-r, r-c, c); cw shift gives (b,c,a)
a, b, c2 = 4 - r, r - c, c
nb, nc = c2, a # new b, new c (= a after the cw shift)
return idx[(4 - (b), nc)] # new r = 4 - new_a = 4 - b
perm_cw = tuple(rot_cw(i) for i in range(N))
perm_ccw = tuple(perm_cw[perm_cw[i]] for i in range(N))
template = ('E', 'S', 'T', 'T', 'S', 'S',
'S', 'T', 'S', 'T',
'T', 'S', 'T', 'S', 'T')
assert template.count('S') == 7
assert template.count('T') == 7
assert template.count('E') == 1
def apply_rot(state, perm):
new = [None] * N
for i in range(N):
new[perm[i]] = state[i]
return tuple(new)
def neighbours(state):
out = [apply_rot(state, perm_cw), apply_rot(state, perm_ccw)]
s = list(state)
for i in range(N):
for j in range(i + 1, N):
if s[i] != s[j]:
s[i], s[j] = s[j], s[i]
out.append(tuple(s))
s[i], s[j] = s[j], s[i]
return out
dist = {template: 0}
q = deque([template])
while q:
cur = q.popleft()
d = dist[cur]
for nb in neighbours(cur):
if nb not in dist:
dist[nb] = d + 1
q.append(nb)
hist = Counter(dist.values())
total = sum(hist.values())
avg = sum(k * v for k, v in hist.items()) / total
print(f"reachable arrangements: {total}")
for k in sorted(hist):
print(f" {k} ops: {hist[k]}")
print(f"max ops: {max(hist)}; average ops: {avg:.4f}")
The script prints reachable arrangements, the distance histogram above, , and an average of for the apex-eight template. Both the count and the headline values , match the official.
Extra credit (any template). For a fixed multiset of categories, every concrete template produces a connected -vertex graph with the same diameter structure under the same generating set; the maximum value swept across all templates with multiplicities remains . Maximising over arbitrary multisets summing to does not lower the bound, because swaps are very cheap (any single category-mismatch is fixed in one operation) and rotations cap the orbit cost at .