Chapter 184
When Will Your House Collapse? Should You Take A Construction Contract?
Riddler Express
You live in a row house, one of on your side of the block. After the city is evacuated, the worst-maintained house collapses two years later. Every two years thereafter, two new collapses happen: any house adjacent to one already collapsed falls, and the next-worst-maintained still-standing house falls. (Maintenance rankings are fixed at evacuation.) Assuming a random ranking, what is the longest your house can stand, and the fewest years until all are rubble? How do these answers change for houses?
The Riddler, FiveThirtyEight, June 1, 2018(original post)
Solution
Number the houses along the street, and let house be the th worst-maintained ( falls in round , etc.). In round (at year ), two things happen: rank- house collapses if still standing, and every standing house adjacent to a fallen house collapses.
Longest survival. You want to delay your collapse. Make your own house the best-maintained ( = your house), and arrange the rankings so the contagion creeps along the street away from you. Put your house at one end, say position . Place at position (far end). For the maintenance-driven collapses not to leapfrog the contagion, place in positions in order. Then in round the contagion reaches position at the same time the rank- house at that position is scheduled to fall; the wavefront moves one position per round. Your house falls in round , that is, year . For , that is .
This is the best you can do: each round at least one new position collapses, and the only positions still standing form a contiguous block at your end (otherwise a standing house would be adjacent to a fallen one and fall by contagion). So the block shrinks by at least one per round, and you survive at most rounds.
Fastest total collapse. Now you want to maximise collapses per round. In round the worst house falls (one house). In round the two neighbours of fall by contagion, plus somewhere fresh: three new collapses, if is placed away from . In round the new has two fresh neighbours fall, the original block grows by another two, and starts a third front: five new collapses. The pattern is per round. We need which gives . For , , that is .
To achieve exactly, place each seed strictly in the interior, so contagion eats both neighbours every subsequent round, and arrange the seeds so the blocks merge only at the very end. After rounds the block seeded at round has length . For and six rounds, the final block lengths are , which sum to exactly. Filling from left to right with these lengths gives seeds at positions , , , , , . Every round contagion produces collapses (two per active block) and the new seed adds one, totalling as required.
General .
The computation
Encode the problem: given a maintenance ranking and a house index, simulate the round-by-round collapse and record both the year your house falls and the year all houses fall. Search over rankings to test the bounds.
Implement
simulate(r, h, N)which returns (year house falls, year all houses have fallen).For : try the constructive rankings for the longest-survival and fastest-collapse layouts and confirm they yield and respectively.
Verify the longest-survival bound by random search: no random ranking ever lets house survive past round .
import random
def simulate(rank, h, N):
"""rank[k] = position of the k-th-worst house, k = 0..N-1."""
standing = [True] * (N + 2) # padding at 0 and N+1
year_h = None; year_all = None
for k in range(N):
to_fall = set()
for i in range(1, N + 1):
if standing[i] and (not standing[i-1] or not standing[i+1]):
to_fall.add(i)
if standing[rank[k]]:
to_fall.add(rank[k])
for i in to_fall:
standing[i] = False
if year_h is None and not standing[h]:
year_h = 2 * (k + 1)
if all(not standing[i] for i in range(1, N + 1)):
year_all = 2 * (k + 1)
break
return year_h, year_all
N = 36
# Longest survival: r_k at position k (worst far from your house)
rank = list(range(1, N + 1))
print("longest survival:", simulate(rank, N, N))
# Fastest collapse: seeds at 6, 16, 24, 30, 34, 36
seeds = [6, 16, 24, 30, 34, 36]
rank_fast = seeds + [p for p in range(1, N + 1) if p not in set(seeds)]
print("fastest collapse:", simulate(rank_fast, N, N))
# Random check: bound is 2N = 72
worst = 0; random.seed(0)
for _ in range(20000):
r = list(range(1, N + 1)); random.shuffle(r)
h = random.randint(1, N)
yh, _ = simulate(r, h, N)
if yh and yh > worst: worst = yh
print("max over 20k random:", worst, "<= 2N =", 2 * N)
The constructive longest-survival layout prints (72, 72) and the fastest-collapse layout prints (12, 12). The random search never exceeds , confirming the bound.
Riddler Classic
Four towns sit at the corners of a square of side miles. The state offers million for a road system linking all four towns; road costs million per mile. Can you turn a profit? How does the answer change for five towns at the corners of a regular pentagon, six at a hexagon, and so on?
The Riddler, FiveThirtyEight, June 1, 2018(original post)
Solution
This is a Steiner tree problem: find the shortest network connecting four (or ) points, allowed to add interior junctions. The candidates and their lengths:
Perimeter shape (three sides): miles.
Diagonal shape (two diagonals, single central junction): miles.
shape (two T-junctions on opposite sides): miles.
TIE-fighter / honeycomb (two Steiner points pulled inward): the minimum.
The honeycomb. Place the towns at and the two Steiner points on the -axis at , with . Each Steiner point joins two towns and the central segment, so Setting : so , giving . The minimum length is At each of the two Steiner points, three road segments meet at exactly , the Steiner condition for a minimum. Cost is million; the million contract clears a profit of
General . For a regular -gon of side , the minimum Steiner tree has different structure as grows.
: one Steiner point at the centroid, total length .
: two Steiner points, total length .
: three Steiner points; numerical minimum at is approximately miles, that is .
: the elegant Steiner-point pattern breaks down. The minimum is simply , the perimeter minus one side, with no interior junctions.
At million per mile and a million contract, the profit is positive only for and at : The contract is a profitable bid only at or . For , walk away.
The computation
For the square, verify the minimum symbolically with sympy. For the pentagon, place the five vertices on a unit circle and solve the Steiner-tree placement numerically by free-coordinate optimisation over three Steiner points.
import math
import numpy as np
from sympy import symbols, sqrt, diff, solve, simplify
from scipy.optimize import minimize
# Square: symbolic derivation
a = symbols('a', real=True, positive=True)
D = 4 * sqrt((5 - a)**2 + 25) + 2 * a
print("square Steiner a =", [simplify(c) for c in solve(diff(D, a), a)])
a_star = 5 - 5 / math.sqrt(3)
D_star = 4 * math.sqrt((5 - a_star)**2 + 25) + 2 * a_star
print(f"square length = {D_star:.4f}, profit = ${(28 - D_star)*1e6:,.0f}")
# Pentagon: numerical Steiner tree on 5 vertices
def pentagon(s=10):
R = s / (2 * math.sin(math.pi / 5))
return [(R*math.cos(2*math.pi*k/5 + math.pi/2),
R*math.sin(2*math.pi*k/5 + math.pi/2)) for k in range(5)]
def steiner_length(p, t):
"""Tree: T0-S0-T1, S0-S1-T2, S1-S2, S2-T3, S2-T4."""
s = p.reshape(3, 2)
edges = [(t[0], s[0]), (t[1], s[0]), (s[0], s[1]), (t[2], s[1]),
(s[1], s[2]), (t[3], s[2]), (t[4], s[2])]
return sum(math.hypot(a[0]-b[0], a[1]-b[1]) for a, b in edges)
vs = pentagon()
init = np.array([vs[0][0], vs[0][1], 0.0, 0.0, vs[3][0], vs[3][1]])
res = minimize(steiner_length, init, args=(vs,))
print(f"pentagon length = {res.fun:.4f}, profit = ${(28-res.fun)*1e6:,.0f}")
Sympy prints the critical point , the square length miles, and the profit . The pentagon optimisation prints miles, a M loss. The boxed answer for the square holds, and the strategy switches sign at .