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.
Always right. Run time is random.
Fixed time. Small chance of a wrong answer.
Markov, Chebyshev, Chernoff: "rarely unlucky".
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)).
Worst case over inputs, but average over our coins. The input is fixed first. Then we flip. The adversary cannot see our coins.
| System | Randomized idea |
|---|---|
Python dict, Rust HashMap | seeded hashing (SipHash) |
| OpenSSL, GMP | Miller–Rabin for key generation |
| Cassandra, Chrome, Bigtable | Bloom filters |
| Redis, Presto | HyperLogLog distinct counts |
rsync, git packs | rolling-hash fingerprints |
| Skip lists (LevelDB, Redis) | random tower heights |
| ML training | SGD, 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.
Problem. An array of n bits. Exactly half are 1. Find the index of any 1.
| Strategy | Cost | Always right? |
|---|---|---|
| Deterministic scan | n/2 + 1 probes in the worst case | yes |
| Las Vegas: probe random cells until a 1 | 2 probes on average | yes |
| Monte Carlo: k random probes, then give up | exactly k | fails w.p. 2−k |
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.
"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
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: 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 error | two-sided error | |
|---|---|---|
| says "yes" | always right | maybe wrong |
| says "no" | maybe wrong | maybe wrong |
| boost by | repeat, any "yes" wins | repeat, 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.
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.
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.
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.
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.
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.
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.
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
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).
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
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.
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.
E[C] = Σi<j 2/(j−i+1) = Σd=2n (n−d+1)·2/d
= 2(n+1)Hn − 4n □
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.
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 luck | Compares for n = 7 |
|---|---|
| This run | 11 |
| Always the minimum (worst) | 6+5+4+3+2+1 = 21 |
| Expected: 2(n+1)Hn − 4n | 16 · 2.593 − 28 = 13.49 |
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)
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.)
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
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
Min cut. Split the vertices of an undirected multigraph into two non-empty sides. Minimize the number of edges that cross.
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)
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)).
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}.
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.
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.
With 4 vertices, the first two edges decide everything. There are C(5,2) = 10 equally likely pairs.
| First two edges | Final sides | Cut |
|---|---|---|
| ab+ac, ab+bc, ac+bc | abc | d | 2 ✓ |
| bc+bd, bc+cd, bd+cd | bcd | a | 2 ✓ |
| ab+bd | abd | c | 3 |
| ac+cd | acd | b | 3 |
| ab+cd | ab | cd | 3 |
| ac+bd | ac | bd | 3 |
Success = 6/10 = 0.6. Checked by trying all 120 edge orders in Python.
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.
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
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).
"Bad event must dodge a small set at every step" → multiply the dodge chances. The product often telescopes (Karger).
| So far | Type | Key number |
|---|---|---|
| Find a 1 | LV / MC | 2 probes / error 2−k |
| Majority vote | MC | error ≤ e−k/18 |
| Quicksort | LV | ≈ 1.39 n log2 n compares |
| Quickselect | LV | ≤ 8n expected |
| Karger | MC | ≥ 1/C(n,2) per run |
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.
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.
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.
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.
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.
Common mistake: computing 2560 first, then reducing. Use pow(a, d, n). It reduces at every step.
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
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.
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.
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|
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.
Definition. A family ℋ of functions U → [m] is universal if for all x ≠ y:
Prh∈ℋ[h(x) = h(y)] ≤ 1/m
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.
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.
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
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.
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.
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.
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 item | k* | FP |
|---|---|---|
| 8 | 6 | ≈ 2.2% |
| 10 | 7 | ≈ 0.82% (measured 0.84%) |
| 16 | 11 | ≈ 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.
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.
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) ✓ □
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.
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.
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.
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.
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
Try each one before you open the answer.
Shuffle n cards at random. How many cards land in their original spot, on average?
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.
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?
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.
A cycle has n vertices. What is its min cut, and how many min cuts does it have?
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.
A Las Vegas algorithm has expected time T. Turn it into a Monte Carlo algorithm with error ≤ 1/2.
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.
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.
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².) □
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.
| Class | x ∈ L | x ∉ L | Example |
|---|---|---|---|
| RP | accept w.p. ≥ 1/2 | always reject | COMPOSITES (MR), "is this polynomial non-zero?" |
| co-RP | always accept | reject w.p. ≥ 1/2 | PRIMES (via MR), PIT |
| ZPP | always right, expected poly time | RP ∩ co-RP | |
| BPP | accept w.p. ≥ 2/3 | reject w.p. ≥ 2/3 | PIT, approx counting |
| PP | accept w.p. > 1/2 | reject w.p. ≥ 1/2 | MAJ-SAT |
P ⊆ ZPP = RP ∩ co-RP ⊆ RP ⊆ BPP ⊆ PP ⊆ PSPACE
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".
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.
| Problem | Randomized | Deterministic |
|---|---|---|
| PRIMES | Miller–Rabin (1976) | AKS (2002) |
| Undirected s-t path in log space | random walk (1979) | Reingold (2005) |
| Near-linear-time min cut | Karger (2000) | Kawarabayashi–Thorup (2015, simple graphs) |
| PIT | Schwartz–Zippel | open |
You store random 64-bit hashes. About how many items until a collision is a 50/50 bet?
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.
Throw 100 balls into 100 bins at random. How many bins stay empty, on average?
Each bin is empty w.p. (1 − 1/100)100 ≈ 0.366. By linearity: 100 · 0.366 = 36.6 bins, close to n/e.
Freivalds says "AB ≠ C" only when it is sure. Which class is the language {(A,B,C) : AB = C} in?
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.)
for _ in range(k): if not freivalds(A, B, C, rng=Random(42)): return False
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.
random is a Mersenne Twister. Its state can be recovered from 624 outputs. Use secrets or os.urandom for tokens and keys.
A fixed seed for hashing or pivoting brings back the worst case. Seed per process, and keep the seed secret.
Chernoff needs independence. Markov needs X ≥ 0. Chebyshev needs a finite variance. Check before you apply.
Repeating with the same seed gives the same error. Amplification needs fresh coins each round.
A Las Vegas run can take far longer than E[T]. Use a tail bound, or cap the time and restart.
rand() % n is not uniform unless n divides the RNG range. Python's randrange uses rejection sampling to avoid this.
Miller–Rabin's "prime" is only probable. Use a certificate such as ECPP when you need proof, for example in a published prime.
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.
| Algorithm | Type | Guarantee |
|---|---|---|
| Randomized quicksort | LV | E[C] = 2(n+1)Hn − 4n |
| Quickselect | LV | E[T] ≤ 8n |
| Karger min-cut | MC | success ≥ 2/(n(n−1)) per run |
| Miller–Rabin | MC, one-sided | error ≤ 4−k |
| Freivalds | MC, one-sided | error ≤ 2−k, O(kn²) |
| Schwartz–Zippel | MC, one-sided | error ≤ d/|S| |
| Universal hashing | LV | E[chain] ≤ 1 + n/m |
| Bloom filter | MC, one-sided | FP ≈ (1−e−kn/m)k |
| Reservoir sampling | exact | Pr[in sample] = k/t |
| 2-SAT walk | MC, one-sided | E[flips] ≤ n² |
| Term | Meaning |
|---|---|
| Las Vegas | Always correct. Running time is random. |
| Monte Carlo | Fixed time. The answer may be wrong with small probability. |
| One-sided error | Only one answer ("yes" or "no") can be wrong. |
| Indicator variable | 1 if an event happens, else 0. Its mean is the event's probability. |
| Linearity of expectation | E[X+Y] = E[X] + E[Y], even for dependent X, Y. |
| Tail bound | An upper bound on the chance of being far from the mean. |
| Amplification | Repeat with fresh coins to shrink the error. |
| w.h.p. | "With high probability": at least 1 − 1/nc. |
| Term | Meaning |
|---|---|
| Witness | A value that proves the input is a "no" (e.g. composite). |
| Strong liar | A base that makes a composite pass Miller–Rabin. |
| Contraction | Merge an edge's two ends into one vertex (Karger). |
| Fingerprint | A short random hash that stands in for a big object. |
| Universal family | Hashes where any two keys collide w.p. ≤ 1/m. |
| False positive | Says "yes" when the truth is "no" (Bloom filter). |
| RP / co-RP / BPP / ZPP | Poly-time classes with one-sided, one-sided, two-sided, and zero error. |
| Derandomization | Removing coins while keeping efficiency. |