Hints — Module 04: Random Numbers
Graduated hints, three per stage. Reach for hint 1 first; drop to 3 only when stuck.
Stage 1: The linear congruential method
- A linear congruential generator iterates
X_{n+1} = (a*X_n + c) mod m(§3.2.1). Theorem 3.2.1.2A tells you when it achieves full period m for every seed:ccoprime tom,a ≡ 1modulo every prime dividingm, anda ≡ 1 (mod 4)when4 | m. The period for a small modulus can be found by direct cycle detection. - Store
(x, a, c, m)and encode the word-size modulus2^64asm == 0, handled with wrapping arithmetic; form > 0, reduce the seed and parameters mod m and carry the product inu128to avoid overflow. Forperiod, record the first step at which each state value appears and return the gap when a state repeats — that gap is the cycle length (the tail is not counted). next: ifm == 0,a.wrapping_mul(x).wrapping_add(c); else((a as u128 * x as u128 + c as u128) % m as u128) as u64.period: avec![u64::MAX; m]of first-seen steps; loopif first_seen[x] != MAX { return step - first_seen[x]; }, else record and advance.
Stage 2: The chi-square test
- The chi-square statistic (§3.3.1) measures how far observed category counts stray from what a probability model predicts:
V = sum_s (Y_s - n*p_s)^2 / (n*p_s). Compare V against the table withk-1degrees of freedom. The expected counts must all be reasonably large (Knuth: merge sparse categories first). - Zip observed counts against expected counts, accumulate
(obs - exp)^2 / exp. Guard the preconditions: equal lengths, non-empty, and every expected count strictly positive (a zero expected count would divide by zero). The uniform variant just sets every expected count ton/k. observed.iter().zip(expected).map(|(&o, &e)| { let d = o as f64 - e; d*d/e }).sum(). Forchi_square_uniform:let n: u64 = counts.iter().sum(); let e = n as f64 / counts.len() as f64;then callchi_squarewith a vector ofes.
Stage 3: Shuffling
- Algorithm 3.4.2P (Fisher–Yates) walks position
jfrom the top down and swapsitems[j]with a random position in0..=j— a shrinking range. That shrinking range is exactly what makes the map from draw-sequences to permutations a bijection onto allt!outcomes, hence uniform. The naive version that draws from the full range each time is biased, becausen^ntapes cannot divide evenly amongn!permutations forn > 2. - Take
rng: &mut impl FnMut(u64) -> u64returning a value in0..bound. Loopjfromt-1down to1, drawk = rng(j+1)(uniform in0..=j), swap, decrement. Slices of length < 2 need no work and must consume no randomness. let mut j = t-1; loop { let k = rng(j as u64 + 1) as usize; items.swap(k, j); if j == 1 { return; } j -= 1; }.naive_shuffleis the deliberately-wrong contrast:for i in 0..n { let k = rng(n as u64) as usize; items.swap(i, k); }— keep it to demonstrate the bias table.
Stage 4: Reservoir sampling
- Algorithm 3.4.2R samples
kitems uniformly from a stream of unknown length in one pass and O(k) memory. The invariant (proved by induction): aftertrecords have been seen, each is in the reservoir with probability exactlyk/t. Newly read record numbertenters with probabilityk/t, displacing a uniformly chosen current occupant. - Fill the reservoir with the first
krecords. Thereafter, for each further record incrementt, drawM = rng(t)uniform in0..t, and ifM < koverwritereservoir[M]with the new record. Handle the short-stream case (fewer thankitems) by returning everything read, andk == 0by returning empty without consultingrng. - Prime with
while reservoir.len() < k { match it.next() { Some(x) => reservoir.push(x), None => return reservoir } }, sett = k; thenfor item in it { t += 1; let m = rng(t); if (m as usize) < k { reservoir[m as usize] = item; } }. Return the reservoir.