Randomized Algorithms
Probability as a Tool

Flip coins and you get code that is simpler, faster, and often the only practical option.
Hash tables, primality tests, min-cuts, Bloom filters and streaming all run on randomness.

Las Vegas

Always right. Run time is random.

Monte Carlo

Fixed time. Small chance of a wrong answer.

Tail bounds

Markov, Chebyshev, Chernoff: "rarely unlucky".

Amplification

Repeat k times and the error drops as 2−k.

Proofs and runnable Python. Every number on these slides comes from a real run (Python 3.12, seeded random.Random(2024)).

Roadmap WHERE WE GO

  1. Why randomness helps, and a warm-up
  2. Las Vegas vs Monte Carlo
  3. Probability toolkit and linearity of expectation
  4. Markov, Chebyshev, Chernoff (with a sizing example)
  5. Randomized quicksort: proof and a hand trace
  6. Quicksort measured, and quickselect
  7. Karger's min-cut, by hand and by proof
  8. Checkpoint
  9. Miller–Rabin primality, by hand
  10. Freivalds' matrix check
  11. Fingerprinting and Rabin–Karp
  12. Schwartz–Zippel identity testing
  13. Universal hashing
  14. Skip lists and treaps
  15. Bloom filters
  16. Reservoir sampling
  17. Monte Carlo integration and π
  18. Coupon collector and birthday paradox
  19. Random-walk 2-SAT
  20. BPP, RP, ZPP
  21. Derandomization
  22. Check yourself (two rounds)
  23. Pitfalls, summary, glossary

Why Flip Coins? MOTIVATION

Four reasons

  1. Beat the adversary. A fixed pivot rule has a bad input. A random pivot has no bad input, only rare bad luck.
  2. Simplicity. Karger's min-cut is 10 lines. Deterministic min-cut algorithms are far longer.
  3. Speed. Checking AB = C takes O(n²) with Freivalds. Recomputing takes about O(n2.37).
  4. Space. Bloom filters, HyperLogLog and Count-Min fit huge data in tiny memory.

The key shift

Worst case over inputs, but average over our coins. The input is fixed first. Then we flip. The adversary cannot see our coins.

Where it runs today

SystemRandomized idea
Python dict, Rust HashMapseeded hashing (SipHash)
OpenSSL, GMPMiller–Rabin for key generation
Cassandra, Chrome, BigtableBloom filters
Redis, PrestoHyperLogLog distinct counts
rsync, git packsrolling-hash fingerprints
Skip lists (LevelDB, Redis)random tower heights
ML trainingSGD, dropout, random init

History: Monte Carlo method (Ulam, von Neumann, Metropolis, 1946–49). Rabin (1976) and Solovay–Strassen (1977) primality tests started the modern field.

Warm-up: Find a 1 in a Half-Full Array INTUITION FIRST

Problem. An array of n bits. Exactly half are 1. Find the index of any 1.

StrategyCostAlways right?
Deterministic scann/2 + 1 probes in the worst caseyes
Las Vegas: probe random cells until a 12 probes on averageyes
Monte Carlo: k random probes, then give upexactly kfails w.p. 2−k

Why 2 on average?

Each probe hits a 1 with chance 1/2. The number of probes is geometric, with mean 1/p = 2.

Why can't the scan do better? An adversary sees your fixed order. It puts all the 0s first.

Common mistake

"Random is fast on average inputs." No. Here the input is the worst one. The average is over our coin flips, for every input.

n = 1_000_000
arr = [1] * (n // 2) + [0] * (n // 2); rng.shuffle(arr)

def lv_find(a):                  # Las Vegas: always right, time random
    probes = 0
    while True:
        i = rng.randrange(len(a)); probes += 1
        if a[i] == 1: return i, probes

def mc_find(a, k):               # Monte Carlo: time fixed, may fail
    for _ in range(k):
        i = rng.randrange(len(a))
        if a[i] == 1: return i
    return None

pr = [lv_find(arr)[1] for _ in range(10_000)]
fails = sum(mc_find(arr, 10) is None for _ in range(100_000))
LV: mean probes 2.008, worst 14
MC k=10: fail rate 0.00100, bound 2^-10 = 0.00098

The whole course in one slide

LV: the answer is sure, the time is random. MC: the time is sure, the answer is random. A tiny error (2−10) buys a huge speedup (10 probes vs 500001).

Las Vegas vs Monte Carlo TWO FLAVORS

Las Vegas: output is always correct. Run time T is a random variable. We bound E[T].

Examples: randomized quicksort, quickselect, Rabin–Karp with verify, treaps.

Monte Carlo: run time is bounded. Output is wrong with probability at most δ.

Examples: Miller–Rabin, Freivalds, Karger, Bloom filters, polynomial identity testing.

one-sided errortwo-sided error
says "yes"always rightmaybe wrong
says "no"maybe wrongmaybe wrong
boost byrepeat, any "yes" winsrepeat, take majority

LV → MC. Stop a Las Vegas run after 2E[T] steps and answer "don't know". By Markov, this fails with probability ≤ 1/2.

MC → LV. If you can check an answer fast, repeat the Monte Carlo run until the check passes. With success chance p, you expect 1/p tries.

Amplification

One-sided error δ, repeated k times independently:

Pr[all k wrong] = δk

With δ = 1/4 and k = 40: error below 10−24. A cosmic ray flipping a bit in your RAM is far more likely.

Probability Toolkit BASICS

Linearity of expectation. For any random variables, even dependent ones:

E[X1 + … + Xn] = E[X1] + … + E[Xn]

Indicator trick. If Xi = 1 when event Ai happens, else 0, then E[Xi] = Pr[Ai].

Union bound. Pr[A1 ∪ … ∪ An] ≤ Σ Pr[Ai]. No independence needed.

Geometric trials. If each try succeeds with probability p, you expect 1/p tries.

Worked example: hat-check problem

n people get their hats back in random order. How many get their own hat, on average?

Let Xi = 1 if person i gets their own hat. Then Pr = 1/n.

E[X] = Σ 1/n = 1

The answer is exactly 1, for every n. The Xi are dependent, and it does not matter.

Worked example: MAX-3SAT

Set each variable at random. A clause with 3 distinct literals fails with probability 1/8. So E[satisfied] = 7m/8.

A random variable reaches its mean at least sometimes. So some assignment satisfies ≥ 7/8 of the clauses. That is the probabilistic method: prove existence by showing a random pick works with positive probability.

Tail Bounds: Markov, Chebyshev, Chernoff CONCENTRATION

Markov

If X ≥ 0:

Pr[X ≥ a] ≤ E[X]/a

Proof. E[X] ≥ a·Pr[X ≥ a], since values below a add at least 0. □

Uses only the mean. Weak, but works for anything nonnegative.

Chebyshev

Pr[|X − μ| ≥ a] ≤ Var[X]/a²

Proof. Apply Markov to (X − μ)² with threshold a². □

Needs only pairwise independence. That is why the median trick and pairwise-independent hashes work.

Chernoff / Hoeffding

X = ΣXi, independent, each in [0, 1], μ = E[X]:

Pr[X ≥ (1+ε)μ] ≤ e−ε²μ/3

for 0 < ε ≤ 1

Pr[|X − μ| ≥ t] ≤ 2e−2t²/n

Proof: apply Markov to esX, factor by independence, then pick the best s.

# 1000 fair coins, 20000 trials: how often |heads - 500| >= 50?
dev = sum(abs(sum(rng.random() < .5 for _ in range(1000)) - 500) >= 50
          for _ in range(20000)) / 20000
cheb  = (1000 * .25) / 50**2              # Var = n/4
chern = 2 * math.exp(-2 * 50**2 / 1000)   # Hoeffding
Pr[|X-500|>=50]: measured 0.0018, Chebyshev <= 0.100, Hoeffding <= 0.0135

Read the numbers

Both bounds hold. Chebyshev is 55× loose. Hoeffding is within 8×, and it drops exponentially as n grows.

This is why "repeat and take the majority" works so well. The majority is wrong only if more than half the runs err. Chernoff makes that chance e−Ω(k).

Worked Example: How Many Repeats? CHERNOFF IN USE

Task. An algorithm is right with probability 2/3 on each run. Runs are independent. We run it k times and take the majority. How big must k be for error ≤ 10−6?

Step 1. Let Xi = 1 if run i is wrong. So E[X] = k/3, where X = ΣXi.

Step 2. The majority is wrong only if X ≥ k/2. That is t = k/2 − k/3 = k/6 above the mean.

Step 3. Apply Hoeffding (one-sided):

Pr[X ≥ k/2] ≤ e−2t²/k = e−2(k/6)²/k = e−k/18

Step 4. Solve e−k/18 ≤ 10−6:

k ≥ 18 · ln(106) = 248.7 ⇒ k = 249

p = 2/3
def majority_error(k):           # exact binomial tail, k odd
    return sum(comb(k, i) * p**i * (1-p)**(k-i) for i in range(k//2 + 1))
print("  k   exact     bound e^(-k/18)")
for k in (1, 9, 49, 99, 193, 249):
    print(f"{k:>3}   {majority_error(k):.2e}  {math.exp(-k/18):.2e}")
  k   exact     bound e^(-k/18)
  1   3.33e-01  9.46e-01
  9   1.45e-01  6.07e-01
 49   7.87e-03  6.57e-02
 99   3.09e-04  4.09e-03
193   9.03e-07  2.20e-05
249   2.96e-08  9.82e-07

Read the table

The bound says 249. The exact answer is 193. The bound is safe and within 30%. You get it with no heavy math.

Each extra 18 runs cuts the error by a factor of e. Error falls exponentially in k.

Common mistake: reusing the same seed on each run. Then the runs are not independent, and Chernoff does not apply. Also pick an odd k, or set a tie rule.

Randomized Quicksort: Expected Comparisons PROOF

Theorem. On any input of n distinct keys, randomized quicksort makes

E[C] = 2(n+1)Hn − 4n ≈ 2n ln n ≈ 1.39 n log2 n

comparisons. Here Hn = 1 + 1/2 + … + 1/n.

Proof. Name the keys by sorted rank: z1 < z2 < … < zn. Let Xij = 1 if zi and zj are ever compared.

  • Two keys are compared only if one is the pivot. After that the pivot is gone. So each pair is compared at most once: C = Σi<j Xij.
  • Look at the block Zij = {zi, …, zj}. It stays together until the first pivot from inside it is chosen.
  • If that first pivot is zi or zj, they get compared. If it is anything between, they are split forever.
  • Each of the j−i+1 keys is equally likely to be first: Pr[Xij=1] = 2/(j−i+1).

E[C] = Σi<j 2/(j−i+1) = Σd=2n (n−d+1)·2/d

= 2(n+1)Hn − 4n  □

z_i ·· p ··· z_j block Z_ij (j − i + 1 keys) first pivot strictly inside: z_i and z_j are never compared first pivot is z_i or z_j (2 of j−i+1 cases): compared once

Why this proof is so nice

No recurrence is needed. Linearity of expectation handles the hugely dependent Xij for free.

A matching tail bound also holds: C = O(n log n) with probability 1 − 1/nc.

Quicksort by Hand WORKED EXAMPLE

Sort [7, 3, 9, 1, 5, 8, 2]. Each pivot is picked at random. Here is one run. Every non-pivot is compared once with the pivot.

call [7, 3, 9, 1, 5, 8, 2]   pivot 5   6 compares
     less [3, 1, 2]    more [7, 9, 8]
call [3, 1, 2]               pivot 1   2 compares
     less []           more [3, 2]
call [3, 2]                  pivot 3   1 compare
     less [2]          more []
call [7, 9, 8]               pivot 8   2 compares
     less [7]          more [9]
                                total 11 compares
Pivot luckCompares for n = 7
This run11
Always the minimum (worst)6+5+4+3+2+1 = 21
Expected: 2(n+1)Hn − 4n16 · 2.593 − 28 = 13.49

Connect it to the proof

Sort the values: z1..z7 = 1, 2, 3, 5, 7, 8, 9.

zi and zj are compared only if one of them is the first pivot chosen from {zi, …, zj}. So:

Pr[zi, zj compared] = 2/(j − i + 1)

  • 1 and 9 (z1, z7): chance 2/7. Here pivot 5 came first, so they were split and never met.
  • 3 and 2 (z3, z2): neighbours, chance 2/2 = 1. Neighbours are always compared.

Common mistake

Thinking random pivots make the worst case O(n log n). They do not. The worst case is still 21 here. But no input forces it, and it is very unlikely.

Try it: redo the trace with pivots 2, then 8, then 5. (Answer: 6 + 4 + 2 = 12 compares.)

Quicksort Measured, and Quickselect CODE

def rquicksort(a, counter):
    if len(a) <= 1: return a
    p = a[rng.randrange(len(a))]
    counter[0] += len(a) - 1      # pivot vs every other item
    less = [x for x in a if x < p]
    eq   = [x for x in a if x == p]
    more = [x for x in a if x > p]
    return rquicksort(less, counter) + eq + rquicksort(more, counter)

def H(n): return sum(1 / k for k in range(1, n + 1))
# mean of 20 runs per n, input already sorted: list(range(n))
n      measured    2(n+1)H_n-4n
100           629            648
1000        11131          10986
10000      154135         155772
sorted input n=2000 comparisons: 25328

Sorted input is the classic worst case for "pivot = first element": n(n−1)/2 = 1,999,000 comparisons at n=2000. The random pivot needs 25,328, about 79× fewer.

def quickselect(a, k):
    """k-th smallest (0-based). Expected O(n)."""
    while True:
        p = a[rng.randrange(len(a))]
        less = [x for x in a if x < p]
        eq   = [x for x in a if x == p]
        if k < len(less): a = less
        elif k < len(less) + len(eq): return p
        else:
            k -= len(less) + len(eq)
            a = [x for x in a if x > p]

data = [rng.randint(0, 10**6) for _ in range(10001)]
print("median:", quickselect(data, 5000) == sorted(data)[5000])
median: True

Why O(n) expected

With probability 1/2 the pivot lands in the middle half. Then the array shrinks to at most 3/4. So you expect 2 rounds per shrink:

E[T] ≤ 2n(1 + 3/4 + (3/4)² + …) = 8n

Karger's Min-Cut Algorithm 1993

Min cut. Split the vertices of an undirected multigraph into two non-empty sides. Minimize the number of edges that cross.

Algorithm

  1. While more than 2 super-vertices remain:
  2.   pick a uniformly random edge (u, v);
  3.   contract it: merge u and v, keep parallel edges, drop self-loops.
  4. Return the edges between the last 2 super-vertices.
a b c d contract (a, b) ab c d parallel edges kept repeat until 2 super- vertices remain
def karger(edges, n):
    parent = list(range(n))
    def find(x):
        while parent[x] != x:
            parent[x] = parent[parent[x]]; x = parent[x]
        return x
    order = edges[:]; rng.shuffle(order)   # random order = random contractions
    comps = n
    for u, v in order:
        if comps == 2: break
        ru, rv = find(u), find(v)
        if ru != rv: parent[ru] = rv; comps -= 1
    return sum(find(u) != find(v) for u, v in edges)

Implementation trick

Contracting random edges one by one is the same as scanning edges in a random order. Skip any edge whose ends are already merged. That is Kruskal with random weights, using union-find.

One run takes O(m α(n)).

Karger by Hand: One Win, One Loss WORKED EXAMPLE

Graph from the last slide: edges ab, ac, bc, bd, cd. Vertices a and d have degree 2. So the min cut is 2, and there are two of them: {a}|{bcd} and {d}|{abc}.

Run 1: success

1. Pick bc (1 of 5). Merge: a, bc, d. Edges left: a-bc ×2, bc-d ×2.

2. Pick an a-bc edge. Merge: abc, d.

Two super-vertices. Crossing edges: bd, cd. Cut = 2. Correct.

Run 2: failure

1. Pick ab. Merge: ab, c, d.

2. Pick cd. Merge: ab, cd.

Crossing edges: ac, bc, bd. Cut = 3. Too big. We contracted cd, a min-cut edge.

Every outcome, exactly

With 4 vertices, the first two edges decide everything. There are C(5,2) = 10 equally likely pairs.

First two edgesFinal sidesCut
ab+ac, ab+bc, ac+bcabc | d2 ✓
bc+bd, bc+cd, bd+cdbcd | a2 ✓
ab+bdabd | c3
ac+cdacd | b3
ab+cdab | cd3
ac+bdac | bd3

Success = 6/10 = 0.6. Checked by trying all 120 edge orders in Python.

Compare with the theorem

Each min cut survives w.p. ≥ 2/(n(n−1)) = 1/6. Two min cuts give ≥ 1/3. The truth, 0.6, is well above that.

Common mistake: deleting parallel edges after a merge. Keep them. They make a heavy link more likely to be contracted, which is what protects a small cut.

Karger: Why It Works PROOF + RUN

Theorem. One run returns a specific min cut C with probability ≥ 2/(n(n−1)) = 1/C(n,2).

Proof. Let k = |C|. Every vertex has degree ≥ k, or it would be a smaller cut by itself. So the graph has m ≥ nk/2 edges.

At step i, with r = n − i + 1 super-vertices left, the same holds: mi ≥ rk/2. So:

Pr[pick an edge of C] ≤ k/(rk/2) = 2/r

Pr[C survives] ≥ ∏r=3n (1 − 2/r) = ∏ (r−2)/r

= (1/3)(2/4)(3/5)…((n−2)/n) = 2/(n(n−1))  □

The product telescopes. A bonus corollary: any graph has at most C(n,2) distinct min cuts.

# two 5-cliques joined by 2 edges: min cut = 2
E, n = two_cliques(), 10
hits = sum(karger(E, n) == 2 for _ in range(10000))
T = math.ceil(n * (n - 1) / 2 * math.log(100))
best = min(karger(E, n) for _ in range(T))
single run success: 0.274   bound 2/(n(n-1)) = 0.022
repeat 208 times -> min cut 2

Amplify

Run T = C(n,2)·ln(1/δ) times and keep the best:

Pr[all fail] ≤ (1 − 1/C(n,2))T ≤ e−ln(1/δ) = δ

Here δ = 1/100, so T = 208. The real success rate (27%) beats the bound (2.2%). This graph has one obvious min cut.

Karger–Stein (1996): contract only down to n/√2, then recurse twice. Success is Ω(1/log n), total time O(n² log³ n).

Checkpoint: The Toolkit So Far RECAP

Four ideas you now own

  1. Two kinds of luck. Las Vegas gambles with time. Monte Carlo gambles with the answer.
  2. Indicators + linearity. Write the cost as a sum of 0/1 variables. Add up their probabilities. No independence needed. (Quicksort.)
  3. Tail bounds. Markov, then Chebyshev, then Chernoff. Chernoff tells you how many repeats to buy.
  4. Amplify. A weak success chance p becomes 1 − δ after about (1/p) ln(1/δ) runs. (Karger.)

Pattern to spot

"Bad event must dodge a small set at every step" → multiply the dodge chances. The product often telescopes (Karger).

So farTypeKey number
Find a 1LV / MC2 probes / error 2−k
Majority voteMCerror ≤ e−k/18
QuicksortLV≈ 1.39 n log2 n compares
QuickselectLV≤ 8n expected
KargerMC≥ 1/C(n,2) per run

Quick self-test

Can you say, without notes, why Karger's product is ∏(r−2)/r? If not, go back one slide before moving on.

Next: algorithms that check instead of compute. Primes, matrices, strings, polynomials.

Miller–Rabin Primality Test MONTE CARLO

def is_probable_prime(n, k=20):
    if n < 2: return False
    for p in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29):
        if n % p == 0: return n == p
    d, s = n - 1, 0
    while d % 2 == 0: d //= 2; s += 1   # n-1 = 2^s * d, d odd
    for _ in range(k):
        a = rng.randrange(2, n - 1)
        x = pow(a, d, n)
        if x in (1, n - 1): continue
        for _ in range(s - 1):
            x = x * x % n
            if x == n - 1: break
        else:
            return False     # a is a witness: n is composite
    return True

print(is_probable_prime(2**127 - 1), is_probable_prime(561),
      is_probable_prime(2**127 + 1))
True False False

Tested against a sieve for every n < 100,000: no mistakes.

The math

If n is prime, then Zn is a field. So x² = 1 has only the roots ±1. By Fermat, an−1 = 1.

So the sequence ad, a2d, …, a2sd starts at 1, or hits −1 before reaching 1.

An a that breaks this pattern is a witness. It proves n is composite.

Theorem (Rabin 1980). If n is an odd composite, at most 1/4 of the bases are strong liars. So k rounds err with probability ≤ 4−k.

n=561: strong liars 8/558 = 0.014
n=1105: strong liars 28/1102 = 0.025
n=8911: strong liars 1780/8908 = 0.200
n=221: strong liars 4/218 = 0.018
Fermat liars for 561: 318 of 558

561 = 3·11·17 is a Carmichael number. It fools the plain Fermat test for every base coprime to it, but Miller–Rabin catches it. Error is one-sided: "composite" is always right.

Miller–Rabin by Hand WORKED EXAMPLE

n = 561, base a = 2

1. Write n − 1 = 560 = 24 · 35. So s = 4, d = 35.

2. Start at 235 mod 561. Then square, mod 561:

2^35  mod 561 = 263
2^70  mod 561 = 166     (263^2 mod 561)
2^140 mod 561 = 67      (166^2 mod 561)
2^280 mod 561 = 1       (67^2  mod 561)
2^560 mod 561 = 1       Fermat test: "looks prime"

3. We never saw 560. And 67² = 1 but 67 ≠ ±1. That is a nontrivial square root of 1. No prime allows one. So 561 is composite.

Bonus: gcd(67−1, 561) = 33 and gcd(67+1, 561) = 17. The witness even hands us factors: 561 = 3 · 11 · 17.

Contrast: n = 97 (prime), a = 5

96 = 25 · 3, so s = 5, d = 3.

5^3  mod 97 = 28
5^6  mod 97 = 8
5^12 mod 97 = 64
5^24 mod 97 = 22
5^48 mod 97 = 96   = n-1, i.e. -1  -> pass

It hits −1, the pattern a prime allows. 97 passes.

What to remember

  • Fermat alone is fooled by 561 (2560 = 1).
  • Miller–Rabin also watches the path to 1. The last value before 1 must be −1.

Common mistake: computing 2560 first, then reducing. Use pow(a, d, n). It reduces at every step.

Freivalds' Algorithm: Checking AB = C VERIFICATION

def matvec(A, x):
    return [sum(a * b for a, b in zip(row, x)) for row in A]

def freivalds(A, B, C, k=20):
    n = len(A)
    for _ in range(k):
        r = [rng.randint(0, 1) for _ in range(n)]
        if matvec(A, matvec(B, r)) != matvec(C, r):
            return False          # definitely wrong
    return True                   # right with prob >= 1 - 2^-k
n=120: multiply 0.115s, Freivalds k=20 0.043s, correct C -> True
one-entry error caught by a single round: 0.514

Each round costs 3 matrix-vector products, O(n²). The naive triple-loop multiply costs O(n³), and the gap widens as n grows.

Theorem. If AB ≠ C, then for random r ∈ {0,1}n:

Pr[ABr = Cr] ≤ 1/2

Proof

Let D = AB − C ≠ 0. Pick an entry dij ≠ 0. Row i of Dr is:

(Dr)i = dijrj + S, where S does not involve rj.

Fix all the other coordinates first. Then at most one value of rj ∈ {0,1} makes this zero. So Pr[(Dr)i = 0] ≤ 1/2. □

This "fix everything else, then flip one coin" move is the principle of deferred decisions.

The single-entry error was caught 51.4% of the time. That matches the bound: with one bad entry, (Dr)i ≠ 0 iff rj = 1, which is exactly 1/2.

Fingerprinting and Rabin–Karp HASHING

Fingerprinting idea

Alice and Bob each hold an n-bit file. Are they equal? Sending the whole file costs n bits, and deterministic protocols need that much.

Instead, pick a random prime p ≤ n². Send (p, x mod p), which is only O(log n) bits.

If x ≠ y, then p must divide |x − y| < 2n. That number has fewer than n prime factors, and there are about n²/ln n² primes to pick from. So the error is ≤ O(log n / n).

Rolling hash: treat a window as a number in base b, taken mod q:

h(si+1..i+m) = (h(si..i+m−1) − sibm−1)·b + si+m

Sliding the window costs O(1). Expected total time is O(n + m).

def rabin_karp(text, pat, base=256, mod=(1 << 61) - 1):
    m, n = len(pat), len(text)
    if m > n: return []
    hp = ht = 0
    for i in range(m):
        hp = (hp * base + ord(pat[i])) % mod
        ht = (ht * base + ord(text[i])) % mod
    top = pow(base, m - 1, mod)
    hits = []
    for i in range(n - m + 1):
        if hp == ht and text[i:i + m] == pat:  # verify: Las Vegas
            hits.append(i)
        if i + m < n:
            ht = ((ht - ord(text[i]) * top) * base
                  + ord(text[i + m])) % mod
    return hits

print(rabin_karp("abracadabra" * 3, "abra"))
[0, 7, 11, 18, 22, 29]

With the verify step it is Las Vegas: never wrong, only slower on hash collisions. Drop the verify and it becomes Monte Carlo. Also checked against str.startswith on 200 random strings.

The same rolling-hash trick powers rsync block matching and content-defined chunking in backup tools.

Schwartz–Zippel: Polynomial Identity Testing ALGEBRA

Lemma (Schwartz–Zippel, 1979–80). Let f ≠ 0 be a polynomial of total degree d over a field F. Let S ⊆ F be finite. Pick x1, …, xn uniformly from S. Then

Pr[f(x1, …, xn) = 0] ≤ d/|S|

Proof (induction on n)

n = 1: a nonzero degree-d polynomial has at most d roots.

Step: write f = Σi x1i fi(x2..n). Let k be the largest i with fi ≠ 0. Then fk has degree at most d−k, so by induction Pr[fk = 0] ≤ (d−k)/|S|.

If fk ≠ 0, then f is a nonzero degree-k polynomial in x1. It vanishes with probability ≤ k/|S|. Add them up: d/|S|. □

P = (1 << 61) - 1          # a Mersenne prime, field Z_P
def poly_equal(f, g, nvars, trials=10):
    for _ in range(trials):
        xs = [rng.randrange(P) for _ in range(nvars)]
        if f(*xs) % P != g(*xs) % P:
            return False
    return True

f = lambda x, y: (x + y) ** 5
g = lambda x, y: (x**5 + 5*x**4*y + 10*x**3*y**2
                  + 10*x**2*y**3 + 5*x*y**4 + y**5)
h = lambda x, y: g(x, y) + x*y            # off by one term
print(poly_equal(f, g, 2), poly_equal(f, h, 2))
True False

Error per trial is ≤ 5/261. The test file also checks the 4×4 Vandermonde determinant identity this way.

Why it matters

  • Testing whether a symbolic determinant is zero. This gives randomized parallel bipartite matching (Lovász 1979).
  • Core of IP = PSPACE, the PCP theorem, and SNARKs in blockchains.
  • No deterministic poly-time PIT algorithm is known. Finding one would imply new circuit lower bounds (Kabanets–Impagliazzo 2004).

Universal Hashing CARTER–WEGMAN 1979

Definition. A family ℋ of functions U → [m] is universal if for all x ≠ y:

Prh∈ℋ[h(x) = h(y)] ≤ 1/m

A classic family

Pick a prime p > |U|. Let a ∈ {1..p−1} and b ∈ {0..p−1} be random:

ha,b(x) = ((ax + b) mod p) mod m

Proof idea: for x ≠ y, the map (a,b) ↦ (ax+b, ay+b) mod p is a bijection onto pairs (r, s) with r ≠ s. Among those, only about a 1/m fraction satisfy r ≡ s (mod m).

p_ = 2**31 - 1
def make_hash(m):
    a, b = rng.randrange(1, p_), rng.randrange(p_)
    return lambda x: ((a * x + b) % p_) % m
Pr[h(x)=h(y)] measured 0.0100  vs 1/m = 0.0100

Theorem. With chaining and a universal family, the expected cost of any lookup is O(1 + n/m). This holds for every fixed set of n keys.

Proof: the expected chain length at key x is Σy≠x Pr[h(y)=h(x)] ≤ n/m, by linearity.

Why not a fixed hash?

Any fixed h maps some |U|/m keys to one bucket. An attacker who knows h sends exactly those keys.

This is the 2011 "HashDoS" attack on PHP, Java, Python and Ruby web servers. The fix was randomized hashing: Python sets PYTHONHASHSEED at startup and uses SipHash.

Stronger guarantees

  • k-wise independent hashes: a random degree k−1 polynomial. Linear probing needs 5-wise.
  • Perfect hashing (FKS 1984): two levels, worst-case O(1) lookup, O(n) space.
  • Cuckoo hashing: two tables, at most two probes per lookup.

Skip Lists and Treaps: Random Balance DATA STRUCTURES

class SkipList:
    MAXH = 32
    def __init__(self):
        self.head = Node(None, self.MAXH); self.h = 1
    def _height(self):                  # Pr[h >= i] = 2^-(i-1)
        h = 1
        while h < self.MAXH and rng.random() < 0.5: h += 1
        return h
    def insert(self, key):
        update, x = [self.head] * self.MAXH, self.head
        for lvl in reversed(range(self.h)):       # right, then down
            while x.next[lvl] and x.next[lvl].key < key: x = x.next[lvl]
            update[lvl] = x
        h = self._height(); self.h = max(self.h, h); node = Node(key, h)
        for lvl in range(h):                      # splice into h lists
            node.next[lvl] = update[lvl].next[lvl]; update[lvl].next[lvl] = node
    # search: same walk as insert, counting steps right and down
N=  1000: height 10, mean search steps 19.0, 2*log2(N) = 19.9
N=100000: height 18, mean search steps 32.2, 2*log2(N) = 33.2

The idea (Pugh, 1990)

A sorted linked list, plus "express lanes". Each node flips coins for its height. Level i holds about n/2i nodes.

Search starts at the top lane. Go right while the next key is smaller. Then drop down one lane.

Fact. Height is O(log n) w.h.p. A search takes about 2 log2 n steps on average. No rotations, no rebalancing.

Why 2 log2 n? Walk the path backwards. At each node, go up w.p. 1/2 or left w.p. 1/2. So about 2 steps per level, and about log2 n levels.

Treap: a cousin

A BST on keys, and a heap on random priorities. Its shape equals the quicksort recursion tree with pivots in priority order. So expected depth is O(log n), by the quicksort proof.

Used in Redis sorted sets, LevelDB/RocksDB memtables, and Java's ConcurrentSkipListMap. Easy to make lock-free.

Bloom Filters BLOOM 1970

class Bloom:
    def __init__(self, m, k):
        self.m, self.k, self.bits = m, k, bytearray(m)
    def _idx(self, item):
        h = hashlib.sha256(item.encode()).digest()
        h1 = int.from_bytes(h[:8], "big")
        h2 = int.from_bytes(h[8:16], "big") | 1
        return [(h1 + i * h2) % self.m for i in range(self.k)]
    def add(self, item):
        for i in self._idx(item): self.bits[i] = 1
    def __contains__(self, item):
        return all(self.bits[i] for i in self._idx(item))

nitems, mbits = 10000, 100000
k = round(mbits / nitems * math.log(2))           # 7
bf = Bloom(mbits, k)
for i in range(nitems): bf.add(f"user{i}")
fp = sum(f"other{i}" in bf for i in range(100000)) / 100000
m/n = 10, k = 7: measured FP 0.0084, formula 0.0082

Double hashing h1 + i·h2 gives k indices from one digest (Kirsch–Mitzenmacher 2006). All 10,000 inserted keys test positive: no false negatives.

"cat" "dog"? one bit for "dog" is 0, so it is definitely not in the set

False positive rate. After n inserts, a given bit is still 0 with probability (1 − 1/m)kn ≈ e−kn/m. A new item is a false positive if all k of its bits are 1:

FP ≈ (1 − e−kn/m)k

Minimize over k: k* = (m/n) ln 2, giving FP ≈ 0.6185m/n.

bits per itemk*FP
86≈ 2.2%
107≈ 0.82% (measured 0.84%)
1611≈ 0.046%

Uses: LSM-tree lookups (RocksDB, Cassandra), CDN "one-hit wonder" filters, Chrome's old Safe Browsing list. Variants: counting Bloom filters (allow deletes), cuckoo and xor filters.

Reservoir Sampling STREAMING

Problem. A stream of unknown length goes by once. Keep a uniform random sample of k items using O(k) memory.

def reservoir(stream, k):          # Algorithm R (Vitter 1985)
    R = []
    for i, x in enumerate(stream):
        if i < k:
            R.append(x)
        else:
            j = rng.randrange(i + 1)   # 0..i
            if j < k:
                R[j] = x               # keep x with prob k/(i+1)
    return R

counts = [0] * 10
for _ in range(50000):
    for x in reservoir(range(10), 3): counts[x] += 1
print(counts)                          # expect 15000 each
[14818, 14899, 15001, 15095, 15047, 14681, 15058, 15104, 15167, 15130]

Each count should be 50000 × 3/10 = 15000. The standard deviation is about 102. Nine counts sit within 1.5σ. One (14681) is 3.1σ low: unlucky, not a bug. With ten counts, one outlier like that turns up now and then. Rerun with other seeds before you suspect the code.

Invariant. After seeing t ≥ k items, each one is in R with probability exactly k/t.

Proof by induction

New item t+1: it enters with probability k/(t+1). ✓

Old item in R: it stays unless the new item enters and lands on its slot:

(k/t)·(1 − (k/(t+1))·(1/k)) = (k/t)·(t/(t+1)) = k/(t+1) ✓ □

More streaming tricks

  • Weighted: give each item the key u1/w and keep the top-k (Efraimidis–Spirakis).
  • Algorithm L: jump ahead by random gaps. That takes O(k log(n/k)) random numbers.
  • HyperLogLog: counts distinct items with 1.5 KB at about 2% error.
  • Count-Min sketch: heavy hitters with O(1/ε · log 1/δ) space.

Monte Carlo Integration: Estimating π SIMULATION

for N in (10**3, 10**4, 10**5, 10**6):
    inside = sum(rng.random()**2 + rng.random()**2 < 1
                 for _ in range(N))
    est = 4 * inside / N
    print(f"{N:<9} {est:.5f}   {abs(est - math.pi):.5f}")
N         estimate    error
1000      3.21600   0.07441
10000     3.14800   0.00641
100000    3.13116   0.01043
1000000   3.14318   0.00159

Each point lands in the quarter circle with probability π/4. The estimator 4·inside/N is unbiased.

Error is not monotone: N = 105 was worse than 104 in this run. Randomness only promises the typical error.

outin

Error rate. The standard error is σ/√N. Here σ = 4√(p(1−p)) ≈ 1.64.

At N = 106 that is about 0.0016, which matches the 0.00159 we saw.

The 1/√N law

Each extra digit of accuracy costs 100× more samples. But that rate does not depend on dimension. Grid methods in d dimensions need N1/d points per side.

That is why Monte Carlo rules physics, finance (option pricing), rendering (path tracing) and Bayesian inference (MCMC). Variance reduction and quasi-random sequences improve the constant.

Coupon Collector and Birthday Paradox CLASSICS

Coupon collector

Each draw gives one of n coupons at random. How many draws until you have all of them?

Once you hold i coupons, a new one arrives with probability (n−i)/n. That phase takes n/(n−i) draws on average. Add the phases:

E[T] = Σi=0n−1 n/(n−i) = nHn ≈ n ln n

def coupons(n):
    seen, t = set(), 0
    while len(seen) < n:
        seen.add(rng.randrange(n)); t += 1
    return t
coupons n=10: mean 29.2, n*H_n = 29.3
coupons n=100: mean 518.5, n*H_n = 518.7
coupons n=1000: mean 7497.1, n*H_n = 7485.5

Shows up in load balancing: n balls into n bins leaves about n/e bins empty. Filling every bin needs n ln n balls.

Birthday paradox

k people, 365 days. The chance that all birthdays differ is:

∏i=0k−1 (1 − i/365) ≈ e−k²/730

A collision passes 50% at k = 23. In general that happens at k ≈ 1.18√n.

def birthday_trial(n_people, days=365):
    return len({rng.randrange(days) for _ in range(n_people)}) < n_people
10 people: simulated 0.117, exact 0.117
23 people: simulated 0.501, exact 0.507
50 people: simulated 0.970, exact 0.970

Why it matters

  • An n-bit hash has collisions after about 2n/2 tries. That is why SHA-256 exists and 64-bit IDs collide.
  • Pollard's rho factoring and rho discrete log both run on the birthday bound.
  • Random UUIDv4 has 122 random bits. You need about 2.7×1018 of them for a 50% collision chance.

Check Yourself (1) EXERCISES

Try each one before you open the answer.

1. Fixed points

Shuffle n cards at random. How many cards land in their original spot, on average?

Answer

Exactly 1, for every n. Let Xi = 1 if card i stays put. E[Xi] = 1/n. By linearity, E[X] = n · 1/n = 1. The Xi are dependent, and it does not matter.

2. One-sided error

A Monte Carlo test is wrong at most 1/3 of the time, and only on "yes" inputs. How many independent runs push the error below 10−9?

Answer

Say "no" if any run says "no". Error = (1/3)k. Need k ≥ 9/log103 = 18.9, so k = 19. No majority vote needed: one-sided error amplifies faster.

3. Karger on a cycle

A cycle has n vertices. What is its min cut, and how many min cuts does it have?

Answer

Min cut = 2. Any 2 of the n edges form one, so there are C(n,2). This shows Karger's bound "at most C(n,2) min cuts" is tight.

4. LV to MC

A Las Vegas algorithm has expected time T. Turn it into a Monte Carlo algorithm with error ≤ 1/2.

Answer

Stop it after 2T steps and output anything. By Markov, Pr[time ≥ 2T] ≤ 1/2. Going back (MC to LV) needs a fast way to check the answer.

Random-Walk 2-SAT PAPADIMITRIOU 1991

def walk_2sat(cnf, n, max_steps=None):
    max_steps = max_steps or 100 * n * n
    a = {v: rng.random() < .5 for v in range(1, n + 1)}
    for step in range(max_steps):
        bad = [c for c in cnf
               if not any(a[abs(l)] == (l > 0) for l in c)]
        if not bad: return a, step
        l = rng.choice(rng.choice(bad))   # random literal, random bad clause
        a[abs(l)] = not a[abs(l)]
    return None, max_steps

# 50 random satisfiable 2-CNFs, n=50 vars, 150 clauses
# (clauses sampled to agree with a hidden assignment)
2-SAT walk n=50: mean flips 141, max 416, budget 2n^2 = 5000

Each flip here rescans all clauses, which is fine for a demo. A real version keeps a list of unsatisfied clauses.

Theorem. If φ is satisfiable, the walk finds a solution in ≤ n² expected flips. So running for 2n² flips fails with probability ≤ 1/2, by Markov.

Proof: gambler's ruin

Fix a solution a*. Let X = the number of variables where a agrees with a*.

An unsatisfied clause has 2 literals, and a* makes at least one true. So flipping a random one of them raises X with probability ≥ 1/2.

That is a walk on {0, …, n} with an upward drift. It hits n in expected ≤ n² steps. (Fair walk: hj = n² − j².) □

0Xn (solved) ≥ 1/2 ≤ 1/2

Schöning (1999) used the same idea for 3-SAT. Restart every 3n flips and it runs in O((4/3)n), far better than 2n. WalkSAT is the practical heir.

BPP, RP, ZPP: Randomized Complexity Classes THEORY

Classx ∈ Lx ∉ LExample
RPaccept w.p. ≥ 1/2always rejectCOMPOSITES (MR), "is this polynomial non-zero?"
co-RPalways acceptreject w.p. ≥ 1/2PRIMES (via MR), PIT
ZPPalways right, expected poly timeRP ∩ co-RP
BPPaccept w.p. ≥ 2/3reject w.p. ≥ 2/3PIT, approx counting
PPaccept w.p. > 1/2reject w.p. ≥ 1/2MAJ-SAT

Known relations

P ⊆ ZPP = RP ∩ co-RP ⊆ RP ⊆ BPP ⊆ PP ⊆ PSPACE

  • RP ⊆ NP: the accepting coins are a certificate.
  • BPP ⊆ Σ2 ∩ Π2 (Sipser–Gács–Lautemann).
  • BPP ⊆ P/poly (Adleman). Some single good coin string works for every input of length n.
PSPACE PP BPP NP RP P, ZPP

Error reduction for BPP

Run k times and take the majority. By Chernoff, the error is e−Ω(k). So the constant 2/3 is arbitrary: any 1/2 + 1/poly(n) works.

BPP is widely seen as the class of problems that are truly "efficiently solvable".

Derandomization: Is Randomness Needed? OPEN-ISH

Conjecture: P = BPP

Most experts believe randomness gives no more than a polynomial speedup for decision problems.

Impagliazzo–Wigderson (1997): suppose some problem in E = DTIME(2O(n)) needs circuits of size 2Ω(n). Then P = BPP.

"Hardness vs randomness": a hard function can be turned into a pseudorandom generator that fools every small circuit.

Landmark derandomizations

ProblemRandomizedDeterministic
PRIMESMiller–Rabin (1976)AKS (2002)
Undirected s-t path in log spacerandom walk (1979)Reingold (2005)
Near-linear-time min cutKarger (2000)Kawarabayashi–Thorup (2015, simple graphs)
PITSchwartz–Zippelopen

Techniques

  • Method of conditional expectations: fix bits one at a time, always keeping E[good] ≥ the target. This makes the 7/8 MAX-3SAT bound deterministic.
  • Limited independence: if the proof only uses pairwise independence, enumerate a small sample space of size O(n²).
  • Expander walks: reuse random bits along a walk on an expander graph.
  • PRGs: Nisan's generator fools log-space machines with O(log² n) seed bits.

Where randomness is provably needed

  • Cryptography: keys must be unpredictable.
  • Communication: equality testing takes Θ(n) bits deterministically, but O(log n) with randomness.
  • Query and streaming models: exponential separations exist.
  • Distributed: consensus under crash faults in an asynchronous system needs coins (FLP 1985, Ben-Or 1983).

Check Yourself (2) EXERCISES

5. Hash collisions

You store random 64-bit hashes. About how many items until a collision is a 50/50 bet?

Answer

Birthday bound: √(2 ln 2 · 264) ≈ 1.18 · 232 ≈ 5.1 billion. That is why 64-bit IDs are not safe for web-scale dedup. Use 128 bits.

6. Empty bins

Throw 100 balls into 100 bins at random. How many bins stay empty, on average?

Answer

Each bin is empty w.p. (1 − 1/100)100 ≈ 0.366. By linearity: 100 · 0.366 = 36.6 bins, close to n/e.

7. Which class?

Freivalds says "AB ≠ C" only when it is sure. Which class is the language {(A,B,C) : AB = C} in?

Answer

co-RP. If AB = C, it always accepts. If not, it rejects w.p. ≥ 1/2. (It is also in P. Just multiply. Freivalds is only faster.)

8. Spot the bug

for _ in range(k): if not freivalds(A, B, C, rng=Random(42)): return False

Answer

A fresh Random(42) each loop gives the same vector every time. All k runs are one run. The error stays 1/2, not 2−k. Create the RNG once, outside the loop.

Common Pitfalls WATCH OUT

Using a predictable RNG where it matters

random is a Mersenne Twister. Its state can be recovered from 624 outputs. Use secrets or os.urandom for tokens and keys.

Letting the adversary see the coins

A fixed seed for hashing or pivoting brings back the worst case. Seed per process, and keep the seed secret.

Assuming independence you don't have

Chernoff needs independence. Markov needs X ≥ 0. Chebyshev needs a finite variance. Check before you apply.

Reusing the same random bits

Repeating with the same seed gives the same error. Amplification needs fresh coins each round.

Confusing expected with typical

A Las Vegas run can take far longer than E[T]. Use a tail bound, or cap the time and restart.

Modulo bias

rand() % n is not uniform unless n divides the RNG range. Python's randrange uses rejection sampling to avoid this.

Trusting a "probabilistic" answer as proof

Miller–Rabin's "prime" is only probable. Use a certificate such as ECPP when you need proof, for example in a published prime.

Tuning a Bloom filter wrong

Too few bits or the wrong k, and the FP rate explodes as n grows. Size for the final n, with k = (m/n)ln 2.

Summary TAKEAWAYS

AlgorithmTypeGuarantee
Randomized quicksortLVE[C] = 2(n+1)Hn − 4n
QuickselectLVE[T] ≤ 8n
Karger min-cutMCsuccess ≥ 2/(n(n−1)) per run
Miller–RabinMC, one-sidederror ≤ 4−k
FreivaldsMC, one-sidederror ≤ 2−k, O(kn²)
Schwartz–ZippelMC, one-sidederror ≤ d/|S|
Universal hashingLVE[chain] ≤ 1 + n/m
Bloom filterMC, one-sidedFP ≈ (1−e−kn/m)k
Reservoir samplingexactPr[in sample] = k/t
2-SAT walkMC, one-sidedE[flips] ≤ n²

Five things to remember

  1. Randomize to beat the adversary, not the average input.
  2. Linearity of expectation plus indicators solves most analyses.
  3. Repeat independent runs: error drops exponentially.
  4. Markov → Chebyshev → Chernoff. Each needs more assumptions and gives a tighter bound.
  5. Checking is often much cheaper than computing: Freivalds, PIT, fingerprints.

Further reading

  • Motwani & Raghavan, Randomized Algorithms (1995)
  • Mitzenmacher & Upfal, Probability and Computing, 2nd ed.
  • CLRS, ch. 5, 7, 9, 11
  • Alon & Spencer, The Probabilistic Method
  • Vadhan, Pseudorandomness (2012)

Glossary REFERENCE

TermMeaning
Las VegasAlways correct. Running time is random.
Monte CarloFixed time. The answer may be wrong with small probability.
One-sided errorOnly one answer ("yes" or "no") can be wrong.
Indicator variable1 if an event happens, else 0. Its mean is the event's probability.
Linearity of expectationE[X+Y] = E[X] + E[Y], even for dependent X, Y.
Tail boundAn upper bound on the chance of being far from the mean.
AmplificationRepeat with fresh coins to shrink the error.
w.h.p."With high probability": at least 1 − 1/nc.
TermMeaning
WitnessA value that proves the input is a "no" (e.g. composite).
Strong liarA base that makes a composite pass Miller–Rabin.
ContractionMerge an edge's two ends into one vertex (Karger).
FingerprintA short random hash that stands in for a big object.
Universal familyHashes where any two keys collide w.p. ≤ 1/m.
False positiveSays "yes" when the truth is "no" (Bloom filter).
RP / co-RP / BPP / ZPPPoly-time classes with one-sided, one-sided, two-sided, and zero error.
DerandomizationRemoving coins while keeping efficiency.