LC Patterns — Probability & Randomness

This file deepens Pattern 14 of math number theory into a full topic. Combinatorial counting (the numerators and denominators of most probabilities) lives in combinatorics.
Per-pattern template: Intuition → Approach/template → Challenges & pitfalls → LC problems (max 3).

The Master Insight (read before the patterns)

LC probability problems come in exactly two species, and confusing them is the first failure mode:

  1. Design randomness ("implement pick() / shuffle() / rand10()"): you must produce random behavior with a specified distribution — and the graded deliverable is the uniformity proof, not the code. Every one of these has a one-or-two-sentence proof; walking in without it means improvising probability theory live.
  2. Compute a probability/expectation ("probability the knight stays on the board"): the answer is a number — and it's either a DP over states (probabilities flow like DP values, because they are DP values) or a symmetry argument that collapses the whole thing to a one-liner.
Recognition trigger: Random/rand in the API, "pick uniformly", "equally likely", "probability that…", "return the answer within 10⁻⁵" (that tolerance line = float answer = probability DP), streams of unknown length, shuffling. The 10⁻⁵ tolerance also whispers: approximations and asymptotic shortcuts are legal — LC 808 is unsolvable without noticing that.

The four proof tools for design-randomness problems — each is two sentences, rehearse all four:

  • Direct construction: "each outcome is chosen with probability (1/n) by definition of the primitive" (weighted pick: dart lands in a segment with probability ∝ its length).
  • Uniform subset: "I generate uniform over a superset and keep only a subset; conditional on keeping, it's uniform over the subset" (rejection sampling).
  • Telescoping product: "P(item i survives to the end) = (1/i)·∏(1 − 1/j) = 1/n, same for every i" (reservoir).
  • Induction on positions: "each element is equally likely to land in each remaining slot" (Fisher–Yates).

And one law that quietly powers the compute-species: linearity of expectation — E[Σ] = ΣE[·] with no independence required. When an expectation looks entangled, decompose it per element/step and sum.

§0Mechanics & Universal Pitfalls

The primitives (Python)

import random
random.random()            # uniform float in [0, 1)
random.randint(a, b)       # uniform int in [a, b]  — INCLUSIVE both ends
random.randrange(n)        # uniform int in [0, n)  — the off-by-one-safe choice
random.shuffle(lst)        # in-place Fisher–Yates (know what it does inside)
random.choice(lst)         # uniform element

randint inclusivity vs randrange exclusivity causes real interview bugs — pick one idiom (randrange) and stick to it.

Facts to have loaded

  • Geometric distribution: independent trials with success probability p → expected trials = 1/p. This is the running-time analysis of every rejection sampler; compute it aloud (470: p = 40/49, E ≈ 1.22 rounds ≈ 2.45 calls).
  • Expected value of DP on probabilities: transition probabilities multiply along a path, sum over paths — i.e., new[s'] += old[s] · P(s→s'). Probability DP is weighted path counting: same table shapes as the DP files, values in [0,1].
  • Conditional probability / Bayes appear on LC only inside proofs (rejection sampling's "conditional on acceptance"), not as computations — know the phrase, don't over-prepare the math.
  • Floats are fine here (answers accepted within 10⁻⁵) — the one topic in this series where float discipline relaxes. Still: sum probabilities in a consistent order, and never test == 1.0.

Universal pitfalls

  1. Producing randomness without proving its distribution. "It looks shuffled" is not an argument. If you can't prove it, assume it's biased — most plausible-looking random code is (see 384's classic bug below).
  2. Arithmetic remapping of rejected samples — taking x % 10 on a uniform 1..49 without rejecting 41..49 overweights some outputs. Rejection means throw away and retry, never "fix up".
  3. Modulo bias in general: big_uniform % n is only uniform when the range divides evenly. State it; it's the same bug as #2 in different clothes.
  4. Confusing "equally likely elements" with "equally likely outcomes" — 497 (random point in rectangles) weights by integer point count (w+1)(h+1), not by area w·h; the +1 fencepost is the whole problem.
  5. Simulating what has a closed form — Monte Carlo is never the intended LC answer; if you catch yourself proposing simulation, look for the DP or the symmetry.

1Rejection Sampling (uniform from the wrong-shaped primitive)

What it is: You have a fair random tool that produces one shape of output, but you need a fair (uniform) result of a different shape. The move: generate over a bigger space you can hit evenly, keep the sample if it lands in a "good" region, and if it lands in a "bad" region, throw it away and roll again — never try to patch or reuse a bad sample.

Intuition: the whole idea rests on one fact — if you start fair and only keep the samples that land in a subset, what's left is still fair inside that subset. That's also why patching rejects breaks things: a patch squeezes many thrown-away values onto a few outputs, unevenly, which is the exact bias you're avoiding. rand10 from rand7 (470): roll twice to build a fair number 1..49 ((r1−1)·7 + r2, a 7×7 grid); keep ≤ 40 and output x % 10 + 1. Since 40 = 4×10, exactly 4 of the kept values map to each output (a clean 4-to-1 map) → fair. Random point in a circle (478): pick a fair point in the square around the circle; keep it if it's inside the circle, otherwise resample (you keep about π/4 ≈ 78.5% of the time). The tempting shortcut for 478 — pick a random angle and random radius directly — is biased: area grows with radius², so a plain uniform radius crowds points near the center. Fixing it needs r = R·√u; rejection sampling avoids that math entirely.

Approach: loop until you get a keeper; make sure your final map is even — every output must come from the same number of kept inputs (say this check out loud). Cost: if you keep a sample with probability p, you expect 1/p rolls (§0). Follow-up for 470 ("use fewer rand7 calls"): the rejected 41..49 is itself a free fair 1..9 — reuse it with another rand7 to build 1..63, keep ≤ 60, and recurse on the leftovers. Know the idea, don't memorize the exact numbers.

Challenges & pitfalls

the "modulo without rejecting first" bug (§0 pitfall 2 — interviewers bait it on purpose; using all of 1..49 makes some outputs appear 5 times and others 4); picking a keep-region that isn't a whole multiple of the target size (keeping 41..49 breaks the even split); proving fairness as two clear steps (fair inside the kept region + an even k-to-1 map) instead of hand-waving; being ready to compute expected calls when asked (have 2·49/40 ≈ 2.45 ready for 470); and for 478, explaining why the naive angle-and-radius approach is biased — the one sentence is "uniform in radius ≠ uniform in area."

LC 470 Implement Rand10(LC 478 Generate Random Point in a Circle
Example 1: rand10 from rand7 (LC 470) — You have a die that rolls 1–7 fairly. You need one that rolls 1–10 fairly.
  • Roll twice. Treat the two rolls as coordinates on a 7×7 grid → this gives you a fair number from 1 to 49 (via (r1−1)*7 + r2).
  • 49 isn't a multiple of 10, so you can't use all of it. Keep only 1–40; if you get 41–49, discard and start over.
  • Now map 1–40 down to 1–10 with x % 10 + 1. Since 40 = 4×10, exactly 4 of the kept values map to each output. Same count for every output → still fair.
  • The bait interviewers plant: taking the modulo without rejecting first (using all of 1–49). Then some outputs get 5 preimages and others get 4 — biased. The rejection step is what makes the map a clean 4-to-1.
Example 2: random point in a circle (LC 478) — You want a uniformly random point inside a circle, but the easy primitive is "uniform point in a square."
  • Sample a point uniformly in the square that boxes the circle.
  • If it's inside the circle, keep it. If it's in a corner outside, throw it out and resample. (You keep about π/4 ≈ 78.5% of the time.)
  • The alternative — pick a random angle and random radius directly — has a famous trap: area grows with radius², so a plain uniform radius crowds points near the center. You'd need r = R·√u to fix it. Rejection sampling sidesteps that math entirely.

The two things to always say out loud:

  1. The map is even / k-to-1. Every output must have the same number of accepted inputs. (This is exactly why you keep 1–40, not 1–49.)
  2. Cost analysis. If you accept with probability p, you expect 1/p tries (geometric distribution). For rand10: about 2 × 49/40 ≈ 2.45 rand7 calls per output.

2Weighted Sampling via Prefix Sums (the dartboard)

What it is: Pick index i with a chance proportional to its weight w[i] — heavier items get picked more often. Lay all the weights end-to-end on a number line (that's the prefix sums), throw one random dart at the whole line, and binary-search to find which segment it landed in.

Intuition: item i takes up a length-w[i] chunk of a line of total length W, so a random dart lands in it with chance w[i]/W — building it this way is the proof, nothing more to show. Finding "which segment did the dart hit" is just find-the-first-prefix-sum-≥-dart — the same binary search as Pattern 2 of binary search. Rectangles (497) is the 2-D version: weight each rectangle by how many integer points it contains, (w+1)·(h+1) (§0 pitfall 4), dart-pick which rectangle, then pick a random point inside it with divmod — two random picks combining into one fair pick over all the rectangles (this needs the rectangles not to overlap, which the problem guarantees — note that you're relying on it).

Approach: build the prefix array once in the constructor (each query must be O(log n) — the problem is set up to punish an O(n) scan per query). Dart: target = random.randint(1, total); answer is bisect_left(prefix, target). Integer dart + bisect_left fit together cleanly; if you throw a float dart with random.random()·total instead, you need bisect_right-style reasoning — pick one convention and hand-check it on weights = [1] and [1, 3] (the two go-to test cases: a single item must always be picked; the second must come up ~75% of the time).

Challenges & pitfalls

the off-by-one tangle of (is the dart inclusive? is the prefix 0- or 1-based? left or right bisect?) — only the hand-check ritual sorts this out, never staring at it; area-vs-points in 497 (w·h fails on single-point rectangles where w = h = 0!); zero-weight items (they must be impossible to pick — inclusive dart + bisect_left handles this, but verify it); re-summing the prefix on every query (that work belongs in the constructor); and stating the proof in one line — "chance = segment length / total, by construction."

LC 528 Random Pick with WeightLC 497 Random Point in Non-overlapping Rectangles
Example 1: Random Pick with Weight (LC 528) — weights = [1, 3, 2]. You want index 0 one-sixth of the time, index 1 three-sixths, index 2 two-sixths.
  • Add up the weights as you go (prefix sums): [1, 4, 6]. Picture a line from 1 to 6 cut into chunks: [1] | [2,3,4] | [5,6].
  • Throw a dart: pick a random integer from 1 to 6.
  • Find the first prefix sum ≥ the dart (binary search). Dart 1 → index 0. Dart 3 → index 1. Dart 5 → index 2.
  • Chunk lengths are 1, 3, 2 — exactly the weights, so each index comes up with the right chance.
Example 2: Random Point in Non-overlapping Rectangles (LC 497) — a bigger rectangle should be more likely to hold your random point.
  • Weight each rectangle by how many integer points it contains: (x2−x1+1)·(y2−y1+1). (Use point count, not area — a single-dot rectangle has 1 point but 0 area, and area would make it impossible to pick.)
  • Dart-pick which rectangle using the prefix-sum trick above.
  • Inside that rectangle, pick a fair point: a random x in [x1, x2] and a random y in [y1, y2].
  • Say out loud: two fair picks (which rectangle, then where inside) combine into one fair pick over all points — but only because the rectangles don't overlap, so no point can be reached two ways.

3Reservoir Sampling (uniform from a stream of unknown length)

What it is: Pick one random element, each equally likely, from a sequence whose length you don't know ahead of time — in a single pass using O(1) memory. Keep just one candidate; when the i-th element arrives, replace the candidate with probability 1/i.

Intuition: for every element to be equally likely after seeing i of them, the newest one must win with chance 1/i — and each earlier winner must survive with chance (i−1)/i, which the same rule gives you for free. The proof is a chain of fractions that cancel: P(element j is the final pick) = (1/j) · (j/(j+1)) · ((j+1)/(j+2)) ⋯ ((n−1)/n) = 1/n — every numerator cancels the next denominator; practice saying it in one breath, it is the interview. Random pick index (398): same trick, but only on elements equal to the target — count matches m as you scan and replace with chance 1/m; picks fairly among duplicates without ever storing their positions.

Approach: if random.randrange(i) == 0: keep = current (i is the 1-based count — randrange(1) is always 0, so the first element is kept automatically with no special case: the elegant part). Worth mentioning: to pick k elements, keep the first k, then replace a random one of them with chance k/i (LC 382's follow-up). Trade-off to name in 398: a hashmap of value → list of indices gives O(1) queries but O(n) memory; reservoir gives O(n) queries but O(1) memory — which is better depends on how often you query, and naming that trade-off is the senior answer.

Challenges & pitfalls

the proof under pressure (rehearse the cancellation aloud — fumbling it turns a solved problem back into an unsolved one); 1-based vs 0-based counting (the first element must be kept 100% of the time — test i=1 in your head); the pick-k replacement rule (replace a random slot with chance k/i — both halves matter); spotting the trigger ("stream", "unknown length", "single pass", "O(1) space" — any two of these means reservoir); and not forgetting the hashmap alternative in 398 (reservoir is the follow-up answer, the hashmap is the default — present both and let the interviewer choose).

LC 382 Linked List Random NodeLC 398 Random Pick Index
Example 1: Linked List Random Node (LC 382) — you get a linked list, don't know its length, and can only walk it once.
  • Walk it node by node, keeping one chosen value.
  • At the i-th node (1-based), replace chosen with it with chance 1/i. Node 1: keep with chance 1/1 (always). Node 2: replace with chance 1/2. Node 3: replace with chance 1/3. And so on.
  • Every node ends up equally likely (1/n), using a single variable.
  • Trace n=3: node 1 is chosen for sure, then survives node 2 (chance 1/2) and node 3 (chance 2/3) → 1 × 1/2 × 2/3 = 1/3. Node 3 is chosen directly with chance 1/3. Every node 1/3. ✓
Example 2: Random Pick Index (LC 398) — given an array with duplicates, return a random index whose value equals target.
  • Scan the array, keeping a counter m of matches seen so far and a chosen index.
  • Each time you hit target: m += 1, then replace chosen with chance 1/m.
  • Same reservoir idea, but you only count the matching elements — so it picks fairly among the duplicates without ever storing their positions.

4Fisher–Yates & Virtual Shuffles (uniform permutations, uniform without repetition)

What it is: Produce a fully random shuffle (every ordering equally likely), or pick without repeats from a huge or partly-blocked range of indices — using the swap trick: pick a random element from the not-yet-touched part, swap it to the edge, and shrink the untouched part by one. When the array is too big to actually build, run the same steps virtually in a hashmap.

Intuition: Fisher–Yates (384): for i from n−1 down to 0, swap a[i] with a[j] where j is random in [0, i] — position i gets each remaining element with equal chance, so every ordering ends up with chance exactly 1/n! (there are n · (n−1) ⋯ 1 equally likely sequences of choices, one for each ordering — that one-to-one match is the proof). The famous broken version — j random in [0, n−1] every time — makes nⁿ equally likely sequences, and nⁿ can't split evenly into n! orderings (it isn't divisible by n!), so it's biased. Know this counterexample cold; it's the most-asked follow-up. Random flip matrix (519): pick without repeats from n·m virtual cells — draw r = randrange(remaining), and a hashmap fakes "swap r with the last remaining cell" via map[r] = map.get(last, last) — Fisher–Yates where only the touched cells actually exist. Blacklist pick (710): split [0, n) so the allowed indices fill a front block of size n−b — remap each blocked index sitting in the front block to an allowed index in the tail (a one-time hashmap); then just pick randomly in the front block, O(1) per query, O(b) memory.

Approach: 384: keep the original array for reset(); shuffle a copy backward with randrange(i+1). 519: remaining -= 1 after each flip; the two-sided get handles chains of earlier swaps — trace one collision by hand before you trust it. 710: build the remap by walking the blocked-in-front indices against the allowed-in-tail ones (a two-pointer sweep over the sorted blacklist, or a set-difference walk); a query is map.get(randrange(n − b), itself).

Challenges & pitfalls

the broken-shuffle counterexample and its divisibility argument (deliver it in two sentences); randrange(i+1) including i itself (an element must be allowed to stay put — banning self-swaps is also a known biased variant, Sattolo's algorithm, which only ever makes one big cycle); 519's chained hashmap defaults (map.get(last, last) — the double-default is where bugs hide); 710's off-by-ones between "blocked inside the front block" and "allowed inside the tail" (count them first — they're equal, and say why); and seeing all three as one pattern (swap-to-the-edge, real or virtual) instead of three separate tricks.

LC 384 Shuffle an ArrayLC 519 Random Flip MatrixLC 710 Random Pick with Blacklist
Example 1: Shuffle an Array (LC 384) — array [A, B, C], and you want all 6 orderings equally likely.
  • Go from the last slot backward. At slot i, swap with a random slot j in [0, i] (j can equal i, so the element may stay put).
  • Slot 2: pick j in {0,1,2}, swap into slot 2. Slot 1: pick j in {0,1}, swap into slot 1. Slot 0: nothing left to do.
  • That's 3 × 2 × 1 = 6 equally likely choice-sequences, one per ordering → each ordering 1/6.
  • The famous bug: picking j in [0, 2] every step gives 3×3×3 = 27 sequences. 27 doesn't divide evenly by 6, so some orderings show up more than others — biased.
Example 2: Random Flip Matrix (LC 519) — a giant grid where you flip random 0-cells to 1 with no repeats, too big to store.
  • Flatten the grid into one index range 0..(rows·cols−1), and track remaining (starts at the total number of cells).
  • Pick r = random in [0, remaining−1] and output the cell r maps to.
  • "Remove" r by pretending to swap it with the last live cell — but only in a hashmap: map[r] = map.get(last, last), then remaining −= 1.
  • Only the cells you actually touch ever get stored, so memory stays tiny.
Example 3: Random Pick with Blacklist (LC 710) — n indices, some blacklisted; return a random allowed one in O(1).
  • There are n − b allowed indices; keep them conceptually in a "front block" [0, n−b).
  • Any blacklisted index sitting inside the front block gets remapped once (a one-time hashmap) to an allowed index out in the tail [n−b, n).
  • Each query: pick a random index in [0, n−b), and return map.get(it, it).

5Probability DP (Markov chains on small state spaces)

What it is: "What's the probability that ⟨process⟩ ends up in ⟨state⟩ after ⟨k steps / when it stops⟩?" Model it as a set of states plus the chances of moving between them, then fill in a probability table step by step — it's just a DP table with fractions in it.

Intuition: P(in a state after t steps) = sum over every state that could lead here of P(was there at t−1) · P(made that move) — the exact same shape as counting-paths DP, only the weights are fractions; every DP habit (designing the state, choosing direction, rolling arrays) carries over unchanged. Knight (688): state = (moves made so far, square); each of the 8 moves has weight 1/8; probability that walks off the board just disappears (that's what "chance the knight is still on the board" means — you add up whatever's left in the table). New 21 game (837): P(reaching total s) = the average of the previous maxPts reachable values → a sliding-window sum turns O(n·maxPts) into O(n), the same window trick from the DP files applied to probabilities; once a total hits K the player stops drawing, which controls which states keep going. Soup servings (808): memoize P over (a, b) with the four ¼-weighted moves; the twist here is a bit of math, not the structure — the servings tend to empty soup A first, so for large n the answer gets very close to 1, and a cutoff (n above ~a few thousand → return 1.0) is required because the state space blows up and the 10⁻⁵ tolerance explicitly allows it. Divide all portions by 25 first (everything is a multiple of 25 — this shrinks the state space, and say why it loses nothing).

Approach: it's literally your DP checklist: define the state so the future depends only on it (add "moves used so far" when the number of steps is capped); pick push-forward or pull-backward; start with probability 1 at the starting state; weight each move by its probability (each row sums to ≤ 1 — some probability may leak out when the process stops); the answer is the sum over the states you care about. Use a rolling 2-layer table when only step t−1 feeds step t (688). Sanity check to run while deriving: total probability is either kept or drains away — it never grows; if your table adds up to more than 1, some move is being counted twice.

Challenges & pitfalls

handling the stopping condition wrong (837: totals in [K, K+maxPts−1] must drop out of the window — the fenceposts around K are where the real difficulty is); rescaling when you shouldn't (the knight's off-board probability is meant to vanish — don't add it back); missing the large-n shortcut in 808 (without the cutoff it times out — the 10⁻⁵ tolerance is the hint, say that connection out loud); float accumulation order (fine here, but add small-to-large if you're worried); and the meta-move — saying "this is a Markov chain, I'll push the distribution forward with DP" instantly levels up the conversation.

LC 688 Knight Probability in ChessboardLC 837 New 21 GameLC 808 Soup Servings
Example 1: Knight Probability in Chessboard (LC 688) — a knight makes k random moves (each of 8 directions equally likely); what's the chance it's still on the board?
  • Keep a table prob[square] = chance of being on that square. Start with 1.0 on the starting square.
  • Each step, spread every square's probability to its 8 knight-moves, each getting 1/8 of it. Moves that land off the board just vanish (nobody adds them back).
  • After k steps, add up all the probability still on the board — that's the answer.
Example 2: New 21 Game (LC 837) — draw cards worth 1..maxPts until your total reaches K; what's the chance the final total is ≤ N?
  • Keep prob[s] = chance of reaching total s. Each prob[s] is the average of the previous maxPts reachable totals (any of them could draw into s).
  • Once a total hits K you stop drawing, so those totals stop feeding new ones.
  • "Average of a moving window" is a sliding-window sum → O(n) instead of O(n·maxPts).
  • Answer: add up prob[s] for every s from K to N.
Example 3: Soup Servings (LC 808) — two soups, four serving options each with chance 1/4, until one runs out.
  • Divide every amount by 25 first (all quantities are multiples of 25) — this shrinks the state space and changes nothing.
  • Memoize P(a, b) = chance A empties first (plus half credit for a tie), summing the four moves each weighted 1/4.
  • Large-n shortcut: the servings tend to empty A first, so once n is above ~5000 the answer is essentially 1.0 — just return 1.0. The 10⁻⁵ tolerance in the problem is your permission to do this.

6Symmetry & Invariant Arguments (the one-line answers)

What it is: Problems that look like they need heavy case analysis or simulation but fall apart once you spot a symmetry: two outcomes that mirror each other must be equally likely, and a detail that doesn't change the answer can be thrown away. The code ends up trivial; the argument is the whole point.

Intuition: airplane seats (1227): the bumped passenger's chain of displacements can only end at seat 1 or seat n — and nothing in the process treats those two seats differently (every choice along the way handles them identically) → by symmetry each is the final seat with chance ½; answer: 1.0 for n = 1, otherwise exactly 0.5. Working small cases by hand (n = 2, 3) shows the answer is always ½, and then you go find the symmetry that explains it — discovering it in that order is fine, present it that way. Ants on a plank (1503): when two ants collide and both turn around, just relabel them — two ants bouncing off each other looks exactly like two ants passing straight through — so the last fall time is simply the longest solo walk: max(max(left positions), max(n − right positions)). All the collision bookkeeping disappears. Robots (2731): same pass-through trick, with one catch — the positions ignore which robot is which, but the sorted order of the robots is preserved, and that's what the distance-sum needs; erase identities, compute, then re-sort.

Approach: there's no template, there's a method: (1) work tiny cases by hand; (2) notice the answer is suspiciously simple (a constant, a max, a sum); (3) look for the symmetry or "these are interchangeable" fact that explains it; (4) present it as claim → argument → two lines of code. The two reusable symmetry shapes on LC: swappable endpoints (1227's seat 1 vs seat n) and relabelable colliding particles (1503/2731 — also the key to several physics-flavored puzzles).

Challenges & pitfalls

trusting the collapse (an answer of "exactly 0.5" feels too easy — the small cases are your proof; compute n=2: ½, n=3: ½, then commit); stating the interchangeability precisely ("the process never refers to seat n specifically, so swapping seat 1 and seat n turns valid histories into valid histories and keeps the probabilities equal" — rehearse 1227's version); the leftover bookkeeping after erasing identities (2731's re-sort — symmetry removes most of the structure, so check what's left); and knowing when symmetry doesn't apply (uneven rules break it — 808's soups are served unevenly, which is why it's DP, not symmetry; contrast them out loud if both come up).

LC 1227 Airplane Seat Assignment ProbabilityLC 1503 Last Moment Before All Ants Fall Out of a PlankLC 2731 Movement of Robots
Example 1: Airplane Seat Assignment (LC 1227) — the first passenger lost their ticket and sits in a random seat; everyone after takes their own seat if it's free, otherwise a random one. What's the chance the last passenger gets their own seat?
  • The chain of bumping can only ever end at seat 1 (the first passenger's real seat) or seat n (the last passenger's seat).
  • Nothing in the process treats those two seats differently, so by symmetry each is equally likely to be the one left over.
  • Answer: 0.5 for n ≥ 2 (and 1.0 for n = 1). Confidence check: compute n=2 and n=3 by hand — both come out ½.
Example 2: Last Moment Before All Ants Fall Off (LC 1503) — ants walk left or right on a plank; when two collide, both turn around. When does the last ant fall off?
  • Trick: two ants bouncing off each other looks identical to two ants passing straight through (just swap their name tags).
  • So ignore collisions entirely — pretend each ant walks straight to its edge.
  • Answer = the longest single walk: the max over left-movers of their distance to the left edge, and over right-movers of their distance to the right edge.
Example 3: Movement of Robots (LC 2731) — robots move and reverse on collision; sum all pairwise distances after some time.
  • Same pass-through trick: treat collisions as robots passing through each other, so each robot's position is just start ± time.
  • The catch: even though positions cross, the robots stay in the same sorted order — so compute all positions, re-sort them, then sum the pairwise distances.

Summary Table — Recognition Cheat Sheet

#PatternTrigger phrase in problemCore moveThe proof (rehearse it)
1Rejection samplingbuild randX from randY, point in regionsuperset, accept/retry, k-to-1 mapuniform subset of uniform superset
2Weighted pick"pick i with probability w[i]/W"prefix sums + one dart + bisectsegment length / total, by construction
3Reservoirstream, unknown length, one passreplace with probability 1/itelescoping product → 1/n
4Fisher–Yates / virtualshuffle, pick without repetition, blacklistswap random pick to boundary; hashmap if virtualn! equal sequences ↔ permutations; nⁿ ∤ n! kills the naive version
5Probability DP"probability after k steps", 10⁻⁵ tolerancedistribution table, weighted transitionsmass conserved or absorbed
6Symmetrydisplaced passenger, colliding particleserase indistinguishable detailexchange argument on histories

Species check first: design randomness (patterns 1–4: prove the distribution) vs compute a probability (patterns 5–6: DP unless a symmetry collapses it).

Study order: 2 (easiest proof, reuses binary search) → 1 (rejection + the expected-trials analysis) → 4 (the biased-shuffle counterexample is mandatory interview knowledge) → 3 (drill the telescoping proof aloud) → 5 (transfers your whole DP toolkit) → 6 (read the three arguments until each is a two-sentence recital).
The interview sentence for any of these (per your communication playbook §1.3): design species — "I'll construct it from ⟨primitive⟩ and prove uniformity: ⟨two-sentence proof⟩; expected cost is ⟨1/p or log n⟩ per call." Compute species — "The process is a Markov chain over ⟨states⟩ — I'll propagate the distribution forward — unless the symmetry between ⟨X⟩ and ⟨Y⟩ collapses it first; let me check small cases by hand."