LC Patterns — Probability & Randomness
The Master Insight (read before the patterns)
LC probability problems come in exactly two species, and confusing them is the first failure mode:
- 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. - 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.
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 elementrandint 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
- 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).
- Arithmetic remapping of rejected samples — taking
x % 10on a uniform 1..49 without rejecting 41..49 overweights some outputs. Rejection means throw away and retry, never "fix up". - Modulo bias in general:
big_uniform % nis only uniform when the range divides evenly. State it; it's the same bug as #2 in different clothes. - Confusing "equally likely elements" with "equally likely outcomes" — 497 (random point in rectangles) weights by integer point count
(w+1)(h+1), not by areaw·h; the +1 fencepost is the whole problem. - 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."
- 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.
- 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·√uto fix it. Rejection sampling sidesteps that math entirely.
The two things to always say out loud:
- 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.)
- 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."
- 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.
- 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).
- Walk it node by node, keeping one
chosenvalue. - At the i-th node (1-based), replace
chosenwith 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. ✓
- Scan the array, keeping a counter m of matches seen so far and a
chosenindex. - Each time you hit target: m += 1, then replace
chosenwith 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.
- 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.
- 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), thenremaining −= 1. - Only the cells you actually touch ever get stored, so memory stays tiny.
- 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.
- 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.
- Keep
prob[s]= chance of reaching total s. Eachprob[s]is the average of the previousmaxPtsreachable 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.
- 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).
- 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 ½.
- 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.
- 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
| # | Pattern | Trigger phrase in problem | Core move | The proof (rehearse it) |
|---|---|---|---|---|
| 1 | Rejection sampling | build randX from randY, point in region | superset, accept/retry, k-to-1 map | uniform subset of uniform superset |
| 2 | Weighted pick | "pick i with probability w[i]/W" | prefix sums + one dart + bisect | segment length / total, by construction |
| 3 | Reservoir | stream, unknown length, one pass | replace with probability 1/i | telescoping product → 1/n |
| 4 | Fisher–Yates / virtual | shuffle, pick without repetition, blacklist | swap random pick to boundary; hashmap if virtual | n! equal sequences ↔ permutations; nⁿ ∤ n! kills the naive version |
| 5 | Probability DP | "probability after k steps", 10⁻⁵ tolerance | distribution table, weighted transitions | mass conserved or absorbed |
| 6 | Symmetry | displaced passenger, colliding particles | erase indistinguishable detail | exchange 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).