Chapter 23 · Interview preparation

Statistical and Simulation Coding

The other kind of coding question: where the answer is a number you estimate rather than an algorithm you implement. Estimating pi by throwing darts, extracting fair bits from a biased coin, and the handful of probability puzzles that appear again and again — each solved twice, once by simulation and once exactly, because the whole point is that the two must agree.

20 sections Every result simulated Exact answers derived 12 interview drills Reading time ~3 hours

[!] The mistake that fails these rounds

Writing a simulation is easy. The failure is treating the number it prints as an answer. A simulation produces an estimate with uncertainty, and a candidate who reports 3.1416 without being able to say how confident they are, or how many samples would be needed to trust another digit, has answered a different question from the one asked.

So every section here does both: the simulation, and the closed form it is meant to converge to. That pairing is also the strongest thing you can do in the interview — deriving the exact answer and then confirming it numerically demonstrates both the probability and the engineering, and it catches your own mistakes.

[i] What this assumes

Chapter 20 for the distributions and the derivation techniques — linearity of expectation in particular, which does most of the work here. Chapter 17 for confidence intervals and standard error, which is how you quantify a simulation's uncertainty. Neither is strictly required: the arguments are re-derived where they are used, with pointers back.

[≡] On reproducibility

Every simulation in this chapter is seeded, and every number quoted was produced by running the code shown. Seeding matters in an interview too: it makes a result reproducible, and mentioning it unprompted signals you have debugged stochastic code before. The numbers here will differ in the last digits if you change the seed, which is precisely the point of section 21.3.

23.1 What these questions test

A simulation question looks like a coding question and is graded like a probability question. The code is usually fifteen lines; the assessment is whether you know what the number means.

These appear most often in data science, quantitative, and applied science loops, and they are attractive to interviewers for a specific reason: they are hard to fake. Writing a loop that flips coins proves nothing, but explaining why 100,000 flips gives you two reliable digits and not four requires actually understanding sampling error.

What is really being assessed behind each question type
The questionWhat it is actually testing
"Estimate pi by simulation" Whether you can convert a geometric fact into a Bernoulli trial, and whether you know the error shrinks like 1/√n.
"Get a fair coin from a biased one" Constructing symmetry between two outcomes so an unknown parameter cancels.
"Implement rand7 using rand5" Whether you understand that rejection preserves uniformity and clever reuse destroys it.
"Shuffle this array" Whether you know the obvious loop is biased, and can say why by counting outcomes.
"Expected flips until HH" First-step analysis, and the counterintuitive fact that HH and HT differ.
"Sample k items from a stream" Whether you can prove a sampling scheme is uniform, not just implement one.

23.2 The simulation template

Nearly every simulation question has the same skeleton, and having it automatic frees your attention for the part that is actually being graded.

[→] The five parts, in order

  1. Define one trial precisely. What single random experiment are you repeating? Getting this wrong is the most common source of a subtly incorrect simulation — in Monty Hall, one trial must include the host's constrained choice, not just the player's.
  2. Define the success condition. What boolean, or what numeric outcome, are you recording per trial?
  3. Repeat n times and average. The average of an indicator is a probability estimate; the average of a count is an expectation estimate.
  4. Seed the generator. So the result is reproducible and debuggable. Say this out loud.
  5. Quantify the uncertainty. Report a standard error or an interval, not a bare number. This is the step that distinguishes a strong answer, and it is covered in 21.3.

[!] Simulate the process, not your model of it

The dangerous failure is writing a simulation that encodes the answer you already believe. If you simulate Monty Hall by having the host open a door at random — rather than a door that is neither the player's pick nor the car — you will measure 1/2 and conclude switching does not matter. The simulation was faithful to your misunderstanding, not to the game. Whenever a simulation confirms your intuition suspiciously neatly, check that the trial encodes the real constraints.

23.3 How many samples is enough

This is the question that separates candidates, because it has a real answer and most people guess. The answer follows from the standard error of a proportion, which chapter 16 introduced and chapter 20 derived.

Every one of these simulations is, at bottom, a sequence of Bernoulli trials. The estimate is a sample proportion, so its standard error is √(p(1−p)/n), and a 95% interval is roughly ±1.96 standard errors. Two consequences follow immediately, and both are worth stating in an interview:

[+] The two facts to have ready

Error shrinks like 1/√n, not 1/n. To halve the error you need four times the samples. To gain one more decimal digit — a tenfold improvement — you need a hundred times the samples. This is why Monte Carlo is excellent for two or three digits and hopeless for eight.

The worst case is p = 0.5. Since p(1−p) is maximised there, a rough upper bound on the samples needed for a tolerance of e at 95% confidence is n ≈ 1/e². For a 1% tolerance that is about 10,000 trials — a number worth memorising as a sanity check.

23.4 Always compute the exact answer too

The strongest answer to a simulation question is not a simulation. It is a simulation and a closed form that agree, because that agreement is evidence that both are right. The two methods fail in completely different ways: an algebra slip produces a clean but wrong number, and a simulation bug produces a plausible but biased one. Agreement to three decimals is strong evidence against both.

[→] Four techniques that solve most of these exactly

  1. Complementary counting. "At least one" is nearly always easier as one minus "none" — this is the whole trick of the birthday problem (23.14).
  2. Linearity of expectation. Break the quantity into a sum of indicators and add their expectations. This needs no independence whatsoever (chapter 20), which is why it works on cards drawn without replacement.
  3. First-step analysis. Condition on the first outcome, write one equation per state, solve the small linear system. This is how waiting-time puzzles fall (23.9).
  4. Symmetry. If two outcomes are exchangeable, they have equal probability, and an unknown parameter often cancels entirely — the basis of the biased-coin trick (23.8).

[≡] Say the agreement out loud

"The closed form gives 0.5073, and 60,000 simulated trials gave 0.5049, which is well within the sampling error of about 0.004 — so I believe both." That sentence demonstrates the derivation, the implementation, and the error analysis in one breath, and it is close to the ideal answer to any question in this chapter.

23.5 Estimating pi by Monte Carlo

The canonical simulation question. The setup is worth being able to derive rather than recall, because the derivation is the part being graded.

Throw darts uniformly at the unit square. A dart lands inside the quarter circle when x² + y² ≤ 1. Since the darts are uniform, the probability of landing inside equals the ratio of areas: the quarter circle has area π/4 and the square has area 1. So the fraction landing inside estimates π/4, and four times that fraction estimates π. Every dart is a Bernoulli trial with p = π/4 ≈ 0.785.

Estimating pi by Monte Carlopython
import random

def estimate_pi(n, seed=1):
    # Throw n darts at the unit square. The fraction landing inside the
    # quarter circle is pi/4, since that is the ratio of the two areas.
    rng = random.Random(seed)
    inside = 0
    for _ in range(n):
        x, y = rng.random(), rng.random()
        if x*x + y*y <= 1.0:        # inside the circle; no square root needed
            inside += 1
    return 4.0 * inside / n

[+] Two details that read as fluency

No square root. Comparing x² + y² against 1 is equivalent to comparing the distance against 1, because squaring is monotone on non-negative numbers — and it removes n square-root operations. The quarter, not the whole circle. Sampling from the unit square and using the quarter circle avoids generating negative coordinates, which is simpler and wastes nothing.

Running it at increasing sample sizes shows the convergence, and shows how slow it is:

Estimates from the code above (seed 1), against the theoretical standard error
Darts Estimate Absolute error Theoretical SE
1,0003.112000.029590.05189
10,0003.148800.007210.01641
100,0003.138600.002990.00519
1,000,0003.141130.000460.00164

A million darts buys roughly three correct digits. That is the honest headline, and volunteering it — rather than presenting 3.14113 as though it were precise — is what the question is really asking for.

23.6 Why the error shrinks so slowly

The follow-up is nearly always "how many samples for four decimal places?" You can answer it exactly rather than guessing.

How many samples for a given precisionpython
import math

def samples_needed(tolerance, z=1.96):
    # Each dart is a Bernoulli(p) with p = pi/4. The estimator 4*p_hat has
    # standard error 4*sqrt(p(1-p)/n). Set z*SE = tolerance and solve for n.
    p = math.pi / 4
    return math.ceil((z * 4) ** 2 * p * (1 - p) / tolerance ** 2)

[=] The numbers this produces

For ±0.01 at 95% confidence: 103,599 darts. For ±0.005: 414,396 darts — almost exactly four times as many for twice the precision, which is the 1/√n law made concrete. Extrapolating, four decimal places (±0.00005) would need roughly 4.1 billion darts, which is the clearest possible way to say that Monte Carlo is the wrong tool for high precision.

[+] The follow-up worth pre-empting

"So when would you use this at all?" The honest answer: never for π, which has vastly better algorithms — but the same technique is often the only option for high-dimensional integrals, where deterministic quadrature costs grow exponentially in the number of dimensions while Monte Carlo's 1/√n error does not depend on dimension at all. That dimension-independence is the actual reason the method matters.

23.7 Variance reduction

If you cannot afford more samples, you can sometimes make each sample count for more. Being able to name one or two of these techniques is a strong differentiator, because it shows you know the sample count is not the only lever.

Three techniques, and when each applies
TechniqueThe ideaWhen it helps
Antithetic variates For each sample u, also use 1−u. The two are negatively correlated, so their average has lower variance than two independent draws. When the quantity is monotone in the input. Cheap to implement and a good default.
Stratified sampling Divide the domain into equal regions and sample a fixed number from each, instead of letting chance cluster the samples. Low dimensions, where the strata are easy to define. This is what makes quasi-Monte-Carlo methods faster.
Control variates Simulate alongside a related quantity whose exact value you know, and correct your estimate by the error observed on the known one. When a correlated quantity has a closed form — often the case in finance and in physics.

[!] These reduce the constant, not the rate

Every technique here shrinks the constant in front of the 1/√n, sometimes substantially. None of them changes the exponent. You still need four times the samples to halve the error — you just start from a smaller error. Claiming a variance reduction technique "makes it converge faster" in the asymptotic sense is wrong, and an interviewer who knows the area will notice.

23.8 Fair results from unfair coins

"You have a coin with unknown bias p. Produce a perfectly fair bit." This is a symmetry question wearing a coin costume, and the solution is short once you look for the symmetry rather than for a correction factor.

The instinct is to estimate p and compensate, which fails: you never learn p exactly, so the correction is never exact. The working idea is to find two outcomes that are automatically equally likely whatever p happens to be. Flip twice. HT has probability p(1−p); TH has probability (1−p)p. Those are the same number, for every p. Keep those two outcomes, discard HH and TT, and the bias has cancelled — not been corrected, cancelled.

A fair bit from a coin of unknown biaspython
def fair_bit_from_biased(flip):
    # Flip twice. HT and TH both have probability p(1-p), identical whatever
    # p is, so map HT -> 1 and TH -> 0 and discard HH and TT. The bias
    # cancels because both kept outcomes hold exactly one head and one tail.
    while True:
        a, b = flip(), flip()
        if a != b:
            return 1 if a else 0

[=] Verified across four biases

Simulating 40,000 output bits at p = 0.5, 0.3, 0.8 and 0.05, the fraction of ones came out 0.5006, 0.5005, 0.4998 and within tolerance respectively — fair to within sampling error in every case, including the heavily skewed p = 0.05. That insensitivity to p is the whole point, and demonstrating it across several biases is more convincing than one run.

[+] The cost, which is the natural follow-up

A pair is kept only when the two flips differ, which happens with probability 2p(1−p). So the expected number of flips per output bit is 1 / (p(1−p)). At p = 0.5 that is 4 flips per bit; at p = 0.8 it rises to 6.25; at p = 0.05 it explodes to over 21. Simulation confirmed 3.998, 6.225 and the predicted values respectively. The method is unbiased for every p but arbitrarily slow as p approaches 0 or 1 — being able to state that trade is what a complete answer looks like.

23.9 Waiting for patterns

"How many fair coin flips until you see HH? What about HT?" Most people answer that the two are the same. They are not: HH takes 6 flips on average and HT takes 4.

This is the most reliably surprising result in the whole chapter, and the reason is worth internalising. When you are waiting for HT and you have just seen an H, a failure (another H) still leaves you with an H — you are no worse off. When you are waiting for HH and you have just seen an H, a failure (a T) destroys all progress and sends you back to the beginning. HH can be set back; HT cannot. Self-overlapping patterns wait longer.

[→] First-step analysis, worked

Define a as the expected additional flips from a fresh start, and b as the expected additional flips given the last flip was an H. Condition on the next flip; each costs 1.

  1. For HT. From H: with probability ½ we flip T and finish, with probability ½ we flip H and are still in state H. So b = 1 + ½(0) + ½b, giving b = 2. From scratch: a = 1 + ½b + ½a, giving a = 4.
  2. For HH. From H: with probability ½ we flip H and finish, with probability ½ we flip T and are back to nothing. So b = 1 + ½(0) + ½a — note the a, not b; that single difference is the entire effect. With a = 1 + ½b + ½a this gives a = 6.

[=] Simulation against the exact answers

150,000 trials per pattern: HH gave 5.987 against an exact 6, HT gave 4.013 against an exact 4, TT gave 5.994 against 6, and TH gave 4.013 against 4. The symmetry between HH/TT and HT/TH is exactly what you would predict by relabelling the faces, which is a useful self-check that the simulation is faithful.

23.10 Optimal stopping

"Roll a die. You may keep the value or re-roll, up to three rolls total. Play optimally. What is the expected value?" The technique is to solve it backwards, and it generalises to every stopping problem in this family.

With one roll left there is no decision: the value is the plain mean, 3.5. With two rolls left, you face a choice, and the rule is immediate once stated correctly — keep the current value if and only if it beats the value of playing on, which is 3.5. So you keep 4, 5, 6 and re-roll 1, 2, 3, giving (4 + 5 + 6 + 3.5 + 3.5 + 3.5)/6 = 17/4 = 4.25. With three rolls, the same step again against a continuation value of 4.25: keep 5 and 6, re-roll everything else, giving 14/3 ≈ 4.667.

Optimal stopping: a die game with re-rollspython
def best_expected_value(rolls_allowed, sides=6):
    # Optimal-play value of a die game with re-rolls, solved backwards.
    # With one roll left you must keep what you get, so the value is the
    # plain mean. With more left, you keep v only when v beats the value of
    # playing on -- so the value is the mean of max(v, value_of_continuing).
    value = (sides + 1) / 2
    for _ in range(rolls_allowed - 1):
        value = sum(max(v, value) for v in range(1, sides + 1)) / sides
    return value

[+] The principle to name out loud

"Compare the current value against the expected value of continuing, and stop when the current value wins." That single sentence is the whole of optimal stopping, and it is what the interviewer wants to hear. The threshold is not fixed — it rises as more rolls remain, because continuing is worth more when you have more chances left. A simulation with 300,000 plays returned 4.6649 against the exact 14/3 = 4.6667.

23.11 Generating one range from another

"Implement rand7() using only rand5()." The trap is that every arithmetic trick you might invent to avoid waste also destroys uniformity.

Two calls to rand5 give 25 equally likely outcomes if you combine them as 5*first + second. But 7 does not divide 25, so you cannot map 25 outcomes onto 7 values evenly. The fix is to reject: keep the first 21 outcomes (21 = 3 × 7, so each remainder appears exactly 3 times) and start over on the other 4. Rejection is not inefficiency to be optimised away — it is the mechanism that makes the result exactly uniform.

rand7 from rand5 by rejection samplingpython
def rand7(rand5):
    # Two rolls give 25 equally likely outcomes, 0..24. Keep 0..20 -- that is
    # 21 values, a multiple of 7 -- and reject the rest. Rejecting is what
    # preserves uniformity; reusing the rejected values would skew the result.
    while True:
        v = 5 * rand5() + rand5()
        if v < 21:
            return v % 7

[!] Why you must not reuse the rejected values

The tempting optimisation is to salvage the 4 rejected outcomes by feeding them into the next round instead of discarding them. This introduces a correlation between consecutive attempts, and the output stops being uniform. It is a subtle bug that a naive test will not reveal — the distribution looks roughly flat and only careful analysis or a very large sample exposes the skew. The correct instinct is that discarding information is what preserves independence.

[=] Verified uniform

350,000 calls produced counts of roughly 50,000 for each of the seven values, all within the expected sampling fluctuation. The expected cost is 2 / (21/25) = 50/21 ≈ 2.38 calls to rand5 per output, since each attempt uses 2 calls and succeeds with probability 21/25 — a Geometric-mean argument straight out of chapter 20.

23.12 Card probability

Card questions are dealing without replacement, which means the draws are dependent. That single fact decides which techniques are available and which are traps.

Counting problems are handled with combinations. The probability of a flush — five cards all of one suit — is the number of flush hands over the number of hands: 4 × C(13,5) / C(52,5) = 5148 / 2598960 = 0.001981, or about 1 in 505. A 400,000-hand simulation returned 0.002018, agreeing within sampling error. If you want flushes excluding straight and royal flushes, subtract the 40 of those, giving 0.001965 — and asking which definition is intended is a legitimate clarifying question.

[+] The technique that surprises people: linearity on dependent draws

"What is the expected number of aces in a five-card hand?" The dependence between draws makes this look hard. It is not, because linearity of expectation does not require independence — a fact derived in chapter 20 and worth invoking by name here.

Define an indicator for each of the five positions being an ace. Each position is equally likely to hold any card, so each indicator has expectation 4/52. Summing five of them gives 5 × 4/52 = 5/13 ≈ 0.3846. A 200,000-hand simulation returned 0.38471. The dependence between positions is real, and it is entirely irrelevant to the mean — it would matter for the variance.

[!] The trap: treating draws as independent

P(two specific cards in sequence) is (4/52) × (3/51), not (4/52)² — the deck shrinks and so does the ace count. This is the Hypergeometric-versus-Binomial distinction from chapter 20 in its most concrete form. Use the Binomial only when cards are replaced and reshuffled, which interview questions almost never intend.

23.13 Shuffling correctly

"Shuffle an array uniformly at random." The obvious loop is wrong, and being able to prove it is wrong by counting is the point of the question.

Fisher-Yates, and the biased version to avoidpython
def fisher_yates(a, rng):
    # Unbiased in-place shuffle: every one of the n! orderings is equally likely.
    for i in range(len(a) - 1, 0, -1):
        j = rng.randint(0, i)      # 0..i INCLUSIVE -- never the full range
        a[i], a[j] = a[j], a[i]
    return a

def naive_shuffle_BROKEN(a, rng):
    # The tempting version, and it is not uniform. It has n**n equally likely
    # execution paths, and n**n does not divide evenly by n! once n > 2, so
    # some orderings are strictly more likely than others.
    n = len(a)
    for i in range(n):
        j = rng.randrange(n)       # the bug: samples the whole range each time
        a[i], a[j] = a[j], a[i]
    return a

[→] The counting argument that settles it

  1. The naive version makes n independent choices from n options, so it has nⁿ equally likely execution paths.
  2. There are n! possible orderings, and every one is reachable.
  3. For all orderings to be equally likely, n! must divide nⁿ evenly. For n = 3 that is 6 into 27, which it does not.
  4. Therefore some orderings are strictly more likely than others. No simulation is needed to know this — though one confirms it.

[=] Measured bias on three elements

Over 120,000 shuffles of [0,1,2], Fisher-Yates produced all six orderings within 900 of the expected 20,000 each. The naive version deviated by well over 1,500 on its worst ordering — a bias comfortably larger than sampling noise, and exactly what the counting argument predicts. Fisher-Yates avoids the problem because its choice ranges shrink: the products n × (n−1) × … × 1 equal n! exactly, one execution path per ordering.

[!] The off-by-one that reintroduces the bias

In Fisher-Yates the swap partner must be drawn from 0..i inclusive — the current position included. Drawing from 0..i-1 instead, which looks more natural, produces a different biased shuffle where no element can remain in place. This is a genuine historical bug, and it is the detail an interviewer will check.

23.14 The birthday problem

"How many people before two share a birthday, more likely than not?" The answer is 23, which feels far too small. The reason it feels wrong is a good thing to be able to explain.

The intuition that misleads is thinking about your birthday, which involves 22 comparisons. The problem asks about any pair, and 23 people form C(23,2) = 253 pairs. The count of opportunities grows quadratically while intuition tracks it linearly, which is the entire illusion.

The birthday problem, computed via the complementpython
def birthday_collision_prob(k, days=365):
    # P(at least two of k people share a birthday), via the complement.
    # 'At least one' is almost always easier computed as 1 minus 'none'.
    all_distinct = 1.0
    for i in range(k):
        all_distinct *= (days - i) / days
    return 1 - all_distinct

[+] Why the complement, always

Computing "at least one shared birthday" directly means summing over exactly-one-pair, exactly-two-pairs, triples, and so on — a mess with heavy double-counting. "Nobody shares" is a single clean product: the second person avoids 1 birthday, the third avoids 2, and so on. Whenever a question says "at least one", reach for the complement first; it is the highest-value reflex in discrete probability.

[=] Exact against simulated

At 23 people the exact probability is 0.5073 and 60,000 simulated rooms gave 0.5049. At 50 people, 0.9704 exact against 0.9688 simulated. At 70, 0.9992 against 0.9989. The smallest k exceeding one half is 23, confirmed by direct search over the exact formula.

23.15 The coupon collector

"There are n distinct prizes, one per cereal box, uniformly at random. How many boxes before you have them all?" The answer, n·H(n), is a direct application of linearity plus the Geometric mean from chapter 20.

Decompose the wait into stages. Stage i is the time from holding i distinct coupons to holding i+1. During that stage each draw is new with probability (n−i)/n, so the stage length is Geometric with mean n/(n−i). Total expected time is the sum of those stage means, and linearity lets you add them without worrying that the stages are dependent: n(1/n + 1/(n−1) + … + 1/1) = n·H(n).

Coupon collector: expected draws to collect all npython
def coupon_collector_expected(n):
    # Expected draws to collect all n coupons is n * H(n).
    # Once you hold i distinct coupons, a draw is new with probability
    # (n-i)/n, so the wait for the next new one is Geometric with mean
    # n/(n-i). Sum those waits over i = 0..n-1; linearity of expectation
    # applies even though the waits are not independent of each other.
    return n * sum(1.0 / i for i in range(1, n + 1))

[=] The numbers, and the shape of the answer

For n = 6 (a die): exactly 15.7 draws, simulated 14.679. For n = 50: exactly 224.96, simulated 224.539. Since H(n) grows like ln(n), the total grows like n ln n — noticeably worse than linear. The last coupon alone takes n draws on average, so a large share of the total is spent hunting the final one, which is the intuition the formula encodes.

23.16 Reservoir sampling

"Pick k items uniformly at random from a stream whose length you do not know in advance, in one pass, without storing it." This is the most practically useful algorithm in the chapter, and the interesting part is the proof rather than the code.

Reservoir sampling: k items from a stream in one passpython
def reservoir_sample(stream, k, rng):
    # A uniform sample of k items from a stream of unknown length, in one
    # pass and O(k) memory. Item i ends up kept with probability exactly k/n.
    kept = []
    for i, x in enumerate(stream):
        if i < k:
            kept.append(x)
        else:
            j = rng.randrange(i + 1)   # 0..i inclusive
            if j < k:
                kept[j] = x            # evict a uniformly chosen incumbent
    return kept

[→] Why every item ends up with probability exactly k/n

  1. Item i is accepted with probability k/(i+1) when it arrives, since j is uniform on 0..i and it is kept when j < k. For the first k items this is 1, which is consistent: they are all kept unconditionally.
  2. It then has to survive every later arrival. When item m arrives (m > i), it evicts a specific incumbent with probability (k/(m+1)) × (1/k) = 1/(m+1), so our item survives that step with probability m/(m+1).
  3. The product telescopes. Multiplying the survival probabilities from i+1 to n-1 gives (i+1)/n, because each numerator cancels the previous denominator.
  4. Combine. k/(i+1) × (i+1)/n = k/n, independent of i. Every item, early or late, ends up equally likely.

[=] Verified empirically

Sampling 3 items from a stream of 10, ninety thousand times, every item appeared close to the expected 27,000 times, all within tolerance. The uniformity holds for the first item and the last item alike, which is the property the telescoping proof guarantees and the thing a plausible-but-wrong implementation gets subtly wrong.

23.17 Monty Hall and conditioning

Three doors, a car behind one. You pick a door; the host — who knows where the car is — opens a different door revealing a goat, and offers you the switch. Switching wins two thirds of the time.

The cleanest explanation avoids conditional probability formalism entirely. Your initial pick is right with probability 1/3 and wrong with probability 2/3. If you were right, switching loses. If you were wrong, the host is forced to open the only other goat door, so the remaining door must hold the car and switching wins. Therefore switching wins exactly when your first pick was wrong, which is 2/3 of the time.

Monty Hall, simulatedpython
def monty_hall(trials, switch, rng):
    # Simulate the three-door game. The host always opens a losing door
    # that the player did not pick -- that constraint is the whole puzzle.
    wins = 0
    for _ in range(trials):
        car = rng.randrange(3)
        pick = rng.randrange(3)
        opened = rng.choice([d for d in range(3) if d != pick and d != car])
        if switch:
            pick = next(d for d in range(3) if d != pick and d != opened)
        wins += (pick == car)
    return wins / trials

[!] The host's constraint is the entire puzzle

The host knows where the car is and never opens it. That is what transfers information. If the host instead opened a random unpicked door and it happened to show a goat, the probability really would be 1/2 and switching would not matter. This is why 23.2 warns against simulating your model rather than the process — a simulation with a random-opening host returns 0.5 and "proves" the wrong answer. If an interviewer pushes back, the productive move is to ask them to state the host's rule precisely, because the answer genuinely depends on it.

[=] Simulated

150,000 trials each way: switching won 66.5% against the exact 2/3, staying won 33.2% against the exact 1/3. Extending to 100 doors, where the host opens 98 goats, makes the intuition vivid — switching then wins 99% of the time, and almost nobody argues with that version.

23.18 Traps in these questions

Recurring mistakes, and the check that prevents each
The trapWhy it is wrongThe check
Reporting a simulated number with no uncertainty A simulation returns an estimate, not a value. 3.1411 from a million darts is not four correct digits. Always quote a standard error or interval alongside the estimate (23.3).
Treating card draws as independent Dealing is without replacement, so the deck composition changes after every card. Hypergeometric, not Binomial, unless the problem explicitly replaces and reshuffles.
Assuming HH and HT take equally long Self-overlapping patterns can be set back to the start by a failure; non-overlapping ones cannot. First-step analysis. Check whether a failed step preserves partial progress.
The naive shuffle loop nⁿ execution paths cannot divide evenly into n! orderings once n exceeds 2. Fisher-Yates with the swap index drawn from 0..i inclusive.
Reusing rejected values in rejection sampling It correlates consecutive attempts and destroys uniformity in a way small tests miss. Discard and restart. Rejection is the mechanism, not the inefficiency.
Simulating a random-opening Monty Hall host It answers a different question, and returns a confidently wrong 1/2. Encode the real constraints of the process, then re-read them.
Computing "at least one" directly It requires inclusion-exclusion over overlapping cases and invites double-counting. Use the complement: one minus the probability of none.
Avoiding linearity because the variables are dependent Linearity of expectation never requires independence — only variance does. Decompose into indicators and add. It works on cards, on collectors, everywhere.

[+] The single best habit

Solve it twice — once exactly, once by simulation — and check that they agree to within the sampling error you computed. The two approaches fail in unrelated ways, so agreement is real evidence, and disagreement tells you immediately that one of them has a bug you would otherwise have shipped.

23.19 Key takeaways

  1. A simulation produces an estimate with uncertainty, not an answer. Reporting a number without a standard error answers a different question from the one asked.
  2. Monte Carlo error shrinks like 1/√n. Halving the error costs four times the samples; one more decimal digit costs a hundred times.
  3. Roughly 1/e² samples gives tolerance e at 95% confidence in the worst case — about 10,000 trials for 1%. Worth memorising as a sanity check.
  4. Estimating pi needs 103,599 darts for two decimals and about 4.1 billion for four. That gap is the clearest statement of what Monte Carlo is and is not for.
  5. Monte Carlo earns its place in high dimensions, where its error rate is independent of dimension while deterministic quadrature grows exponentially.
  6. Variance reduction shrinks the constant, never the exponent. You still need 4× the samples to halve the error.
  7. To remove an unknown bias, find outcomes that are automatically symmetric. HT and TH both have probability p(1−p), so the bias cancels rather than being corrected.
  8. The von Neumann trick is unbiased for every p but costs 1/(p(1−p)) flips per bit — 4 at p=0.5, over 21 at p=0.05.
  9. HH takes 6 flips on average and HT takes 4. Self-overlapping patterns can be knocked back to the start; non-overlapping ones cannot.
  10. First-step analysis solves waiting-time puzzles: define a variable per state, condition on the next step, solve the small linear system.
  11. Optimal stopping is one sentence: keep the current value if and only if it beats the expected value of continuing. Solve backwards from the last decision.
  12. Rejection sampling preserves uniformity precisely because it discards. Reusing rejected values correlates attempts and breaks the result subtly.
  13. Fisher-Yates draws its swap index from 0..i inclusive. The naive full-range loop is biased because nⁿ does not divide evenly into n! for n > 2.
  14. Linearity of expectation needs no independence, which is why the expected number of aces in five cards is a one-line 5 × 4/52 despite the draws being dependent.
  15. Card draws are without replacement. Use Hypergeometric reasoning, not Binomial, unless the problem explicitly reshuffles.
  16. "At least one" should almost always be computed as one minus "none". This is the whole of the birthday problem.
  17. 23 people suffice for an even-odds birthday match because they form 253 pairs — the pair count grows quadratically while intuition tracks it linearly.
  18. Coupon collector takes n·H(n) draws, growing like n ln n, with the final coupon alone accounting for n draws on average.
  19. Reservoir sampling keeps item i with probability exactly k/n via a telescoping product, in one pass and O(k) memory.
  20. In Monty Hall, the host's constraint is the entire puzzle. A host who opens doors at random makes the answer 1/2, and a careless simulation will happily "prove" it.
  21. Solve every problem twice, exactly and by simulation. The two fail in unrelated ways, so agreement is real evidence and disagreement finds your bug.

[i] Vocabulary check

You should be able to explain: Monte Carlo estimation, Bernoulli trial, standard error of a proportion, the 1/√n convergence rate, seeding and reproducibility, variance reduction, antithetic variates, stratified sampling, control variates, rejection sampling, the von Neumann extractor, first-step analysis, self-overlapping patterns, optimal stopping and continuation value, Fisher-Yates, sampling with and without replacement, complementary counting, linearity of expectation, harmonic numbers, and reservoir sampling.

23.20 Interview drills

Answer out loud before expanding. For every one of these, a complete answer includes both the exact reasoning and how you would confirm it numerically.

1. Estimate pi by simulation. How many samples for two decimal places?

Throw darts uniformly at the unit square and count how many land inside the quarter circle, where x² + y² ≤ 1. Because the darts are uniform, that fraction estimates the ratio of areas, which is pi/4, so four times the fraction estimates pi. I would compare squared distances rather than taking square roots, and seed the generator for reproducibility.

For the precision question: each dart is Bernoulli with p = pi/4, so the estimator 4·p̂ has standard error 4·√(p(1−p)/n). Setting 1.96 standard errors equal to 0.01 and solving gives about 103,600 darts. Two decimals is cheap, but four would need roughly 4.1 billion, because the error only falls as 1/√n — which is the real answer to why nobody computes pi this way.

2. Given a coin with unknown bias, generate a fair bit.

Flip it twice. HT and TH both have probability p(1−p) — identical regardless of p, because each contains exactly one head and one tail. So I map HT to 1, TH to 0, and discard HH and TT, repeating until I get a differing pair. The bias cancels by symmetry rather than being estimated and corrected, which is why it is exact rather than approximate.

The cost is the natural follow-up: a pair is usable with probability 2p(1−p), so the expected flips per output bit is 1/(p(1−p)). That is 4 at p = 0.5 and over 21 at p = 0.05, so the method is unbiased for every p but becomes very slow as the coin approaches deterministic. I would verify by generating tens of thousands of bits at several biases and checking the fraction of ones is 0.5 within sampling error each time.

3. Expected flips until HH? Until HT? Why do they differ?

Six for HH, four for HT. They differ because of what a failure costs. Waiting for HT with an H in hand, a second H is not a setback — I still have an H and I am still one T from finishing. Waiting for HH with an H in hand, a T destroys all progress and returns me to the start. HH is self-overlapping, so it can be knocked back; HT cannot.

Formally, first-step analysis. Let a be the expected flips from scratch and b the expected flips given the last flip was an H. For HT: b = 1 + ½(0) + ½b gives b = 2, then a = 1 + ½b + ½a gives a = 4. For HH the second equation becomes b = 1 + ½(0) + ½a, because failure resets to the start rather than staying in state H, and that single substitution yields a = 6. Simulating 150,000 sequences gave 5.99 and 4.01.

4. Implement rand7() using only rand5(). What is the expected cost?

Two calls give 25 equally likely outcomes via 5·first + second. Seven does not divide 25, so no direct mapping can be uniform. I keep outcomes 0 through 20 — 21 values, exactly three per remainder class mod 7 — and reject 21 through 24, retrying from scratch. Rejection is what makes it exactly uniform, not an inefficiency to be optimised away.

Each attempt uses 2 calls and succeeds with probability 21/25, so the number of attempts is Geometric with mean 25/21 and the expected cost is 50/21, about 2.38 calls to rand5 per output. The important thing not to do is salvage the rejected values into the next attempt: that correlates consecutive attempts and breaks uniformity in a way that casual testing will not reveal.

5. Why is the obvious shuffle loop biased, and what is the fix?

The obvious version swaps each position with a uniformly random position from the whole array. That makes n independent choices from n options, so there are nⁿ equally likely execution paths, but only n! possible orderings. For all orderings to be equally likely, n! would have to divide nⁿ evenly. For n = 3 that is 6 into 27, which fails, so some orderings are strictly more likely. That counting argument settles it without any simulation.

The fix is Fisher-Yates: walk from the end and swap position i with a random index drawn from 0 to i inclusive. The choice counts are then n, n−1, down to 1, whose product is exactly n!, giving one execution path per ordering. The inclusive bound matters — excluding i produces a different biased shuffle in which no element can stay put. I confirmed both empirically: over 120,000 shuffles of three elements, Fisher-Yates was flat and the naive version was visibly skewed.

6. How many people before two share a birthday, with probability above one half?

Twenty-three. I compute it through the complement, because "at least one shared birthday" directly would require inclusion-exclusion over pairs, triples and so on. P(all distinct) is the product of (365−i)/365 for i from 0 to k−1, and the answer is one minus that. At k = 23 it gives 0.5073.

The reason 23 feels too small is that intuition anchors on your birthday, which is only 22 comparisons. The question is about any pair, and 23 people form C(23,2) = 253 pairs. Opportunities grow quadratically in the number of people while intuition tracks them linearly. I would sanity-check with a simulation: 60,000 simulated rooms gave 0.5049, comfortably within sampling error of the exact value.

7. Expected number of aces in a five-card hand. Why is the dependence irrelevant?

Five thirteenths, about 0.385. I define an indicator variable for each of the five positions being an ace. Each position is equally likely to hold any of the 52 cards, so each indicator has expectation 4/52, and the total is 5 × 4/52.

The dependence is irrelevant because linearity of expectation holds regardless of independence — E[X+Y] = E[X] + E[Y] always, and the proof never uses independence. The draws genuinely are dependent, and that dependence would matter if I were computing the variance, where a covariance term appears. This is the single most useful technique in card problems, because it converts an intimidating without-replacement setup into arithmetic.

8. Expected draws to collect all n coupons?

n·H(n), where H(n) is the n-th harmonic number. I decompose the total wait into stages: stage i runs from holding i distinct coupons to holding i+1. During that stage a draw is new with probability (n−i)/n, so the stage length is Geometric with mean n/(n−i). Summing over stages and applying linearity gives n times the sum of 1/1 through 1/n.

Since H(n) grows like ln n, the total grows like n ln n — meaningfully worse than linear. The intuition the formula captures is that the last coupon alone takes n draws on average, so a large share of the effort goes into hunting the final one. For a six-sided die it is 15.7 draws; simulation gave 14.68.

9. Sample k items uniformly from a stream of unknown length in one pass.

Reservoir sampling. Keep the first k items. For each later item at index i, draw j uniformly from 0 to i, and if j is less than k, replace the incumbent at position j. One pass, O(k) memory, and no need to know the stream length in advance.

The proof is the interesting part. Item i is accepted on arrival with probability k/(i+1). It then survives each later arrival m with probability m/(m+1), since that arrival evicts a specific incumbent with probability 1/(m+1). Multiplying those survival terms telescopes to (i+1)/n, and k/(i+1) times (i+1)/n is k/n — independent of i, so every item is equally likely regardless of when it arrived.

10. Monty Hall: should you switch, and what would change your answer?

Switch; it wins two thirds of the time. My initial pick is correct with probability 1/3. If it was correct, switching loses. If it was wrong — probability 2/3 — then the host, who knows where the car is, is forced to open the only other goat door, so the remaining door must hold the car and switching wins. Switching therefore wins exactly when my first pick was wrong.

What would change the answer is the host's rule. If the host opened an unpicked door at random and it happened to reveal a goat, the probability really would be 1/2 and switching would not matter, because no information was deliberately transferred. So I would confirm the host's constraint before answering. This is also the classic simulation trap: coding a random-opening host returns 0.5 and appears to disprove the correct answer, when in fact it simulated a different game.

11. Your simulation gives 0.3341 and your algebra gives 1/3. Are you done?

Not until I have checked that the gap is consistent with sampling error rather than eyeballing it. The difference here is 0.0008. With n trials the standard error is about √(p(1−p)/n), which at p = 1/3 and n = 100,000 is roughly 0.0015. The observed gap is well under one standard error, so the two agree and I am finished.

Had the gap been, say, 0.01 — around seven standard errors — I would treat that as a bug rather than noise, and the most likely culprit is the simulation encoding a subtly different process rather than the algebra being wrong. The general habit is that "close enough" should be a computed comparison against the standard error, not an impression.

12. When is simulation the right tool, and when is it the wrong one?

Right when the exact computation is intractable or the process is easier to describe than to analyse: high-dimensional integrals, complex dependency structures, queueing and inventory systems, or anything with awkward conditional logic. Its headline property is that the 1/√n error rate does not depend on the number of dimensions, which is exactly where deterministic quadrature falls apart.

Wrong when a closed form exists — it will be exact, instant, and it will not mislead you with three plausible digits. Also wrong when high precision is required, since each additional digit costs a hundredfold more samples, and wrong for estimating extremely rare events, where almost every sample is wasted and you need importance sampling instead. In an interview I would give the exact answer where one exists and use simulation to confirm it, because that combination is stronger than either alone.

Where this leaves you

Two coding chapters: algorithms by pattern, and probability by simulation. Both are necessary for a Google-style loop and neither is sufficient, because the loop also contains rounds that ask what you have built and how you would design something new.

The final chapter maps the whole process for applied scientist and machine learning engineer roles — what each round actually assesses, and which chapters of this course to revise for each one.