#[derive(Debug, Clone)]
pub struct Rng(u64);
impl Rng {
pub fn seeded(seed: u64) -> Self {
Rng(seed.wrapping_mul(0x9E37_79B9_7F4A_7C15).max(1))
}
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
pub fn next_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
pub fn below(&mut self, n: usize) -> usize {
match n {
0 => 0,
n => (self.next_u64() % n as u64) as usize,
}
}
}
#[derive(Debug, Clone)]
pub struct AliasTable {
prob: Vec<f64>,
alias: Vec<u32>,
}
impl AliasTable {
pub fn build(weights: &[f64]) -> Option<AliasTable> {
let n = weights.len();
if n == 0 {
return None;
}
let clean: Vec<f64> = weights
.iter()
.map(|w| if w.is_finite() && *w > 0.0 { *w } else { 0.0 })
.collect();
let total: f64 = clean.iter().sum();
if total <= 0.0 {
return None;
}
let mut prob: Vec<f64> = clean.iter().map(|w| w * n as f64 / total).collect();
let mut alias = vec![0u32; n];
let (mut small, mut large): (Vec<usize>, Vec<usize>) = (0..n).partition(|&i| prob[i] < 1.0);
while let (Some(s), Some(l)) = (small.pop(), large.pop()) {
alias[s] = l as u32;
prob[l] = (prob[l] + prob[s]) - 1.0;
match prob[l] < 1.0 {
true => small.push(l),
false => large.push(l),
}
}
for i in small.into_iter().chain(large) {
prob[i] = 1.0;
alias[i] = i as u32;
}
Some(AliasTable { prob, alias })
}
pub fn len(&self) -> usize {
self.prob.len()
}
pub fn is_empty(&self) -> bool {
self.prob.is_empty()
}
pub fn draw(&self, rng: &mut Rng) -> usize {
let i = rng.below(self.prob.len());
match rng.next_f64() < self.prob[i] {
true => i,
false => self.alias[i] as usize,
}
}
}
pub fn best_of_d<F>(table: &AliasTable, rng: &mut Rng, d: usize, cost: F) -> Option<usize>
where
F: Fn(usize) -> f64,
{
if table.is_empty() {
return None;
}
let mut best: Option<(usize, f64)> = None;
for _ in 0..d.max(1) {
let i = table.draw(rng);
let c = cost(i);
if best.is_none_or(|(_, bc)| c < bc) {
best = Some((i, c));
}
}
best.map(|(i, _)| i)
}
#[cfg(test)]
mod tests {
use super::*;
fn counts(weights: &[f64], draws: usize, seed: u64) -> Vec<usize> {
let table = AliasTable::build(weights).expect("weights");
let mut rng = Rng::seeded(seed);
let mut c = vec![0usize; weights.len()];
for _ in 0..draws {
c[table.draw(&mut rng)] += 1;
}
c
}
#[test]
fn draws_land_in_proportion_to_the_weights() {
let weights = [1.0, 3.0, 6.0];
let draws = 200_000;
let c = counts(&weights, draws, 42);
for (i, w) in weights.iter().enumerate() {
let want = w / 10.0;
let got = c[i] as f64 / draws as f64;
assert!(
(got - want).abs() < 0.01,
"index {i}: drawn {got:.3}, weight says {want:.3}"
);
}
}
#[test]
fn a_zero_weight_is_never_drawn() {
let c = counts(&[5.0, 0.0, 5.0], 50_000, 7);
assert_eq!(c[1], 0, "zero weight drawn {} times", c[1]);
assert!(c[0] > 0 && c[2] > 0);
}
#[test]
fn nothing_to_draw_from_is_not_a_panic() {
assert!(AliasTable::build(&[]).is_none());
assert!(AliasTable::build(&[0.0, 0.0]).is_none(), "all drained");
assert!(
AliasTable::build(&[f64::NAN, -1.0, 2.0]).is_some(),
"a bad weight is ignored, the good one still works"
);
}
#[test]
fn the_same_seed_makes_the_same_choices() {
let a = counts(&[1.0, 2.0, 3.0], 1_000, 99);
let b = counts(&[1.0, 2.0, 3.0], 1_000, 99);
let different = counts(&[1.0, 2.0, 3.0], 1_000, 100);
assert_eq!(a, b);
assert_ne!(a, different, "a different seed explores differently");
}
#[test]
fn two_choices_beat_one() {
let weights = vec![1.0; 10];
let table = AliasTable::build(&weights).unwrap();
let cost = |i: usize| if i == 9 { 100.0 } else { 1.0 };
let mut rng = Rng::seeded(5);
let mut bad_with_one = 0;
let mut bad_with_two = 0;
for _ in 0..10_000 {
if best_of_d(&table, &mut rng, 1, cost) == Some(9) {
bad_with_one += 1;
}
if best_of_d(&table, &mut rng, 2, cost) == Some(9) {
bad_with_two += 1;
}
}
assert!(bad_with_one > 700, "one draw: {bad_with_one}");
assert!(
bad_with_two * 5 < bad_with_one,
"two draws should be far better: {bad_with_two} vs {bad_with_one}"
);
}
#[test]
fn best_of_d_picks_the_best_it_saw() {
let table = AliasTable::build(&[1.0, 1.0, 1.0]).unwrap();
let mut rng = Rng::seeded(1);
let chosen = best_of_d(&table, &mut rng, 50, |i| i as f64).unwrap();
assert_eq!(chosen, 0);
}
}
#[cfg(test)]
mod scale_tests {
use super::*;
use std::time::Instant;
#[test]
fn a_draw_costs_the_same_at_a_hundred_workers_and_at_a_million() {
let per_draw = |n: usize| -> f64 {
let table = AliasTable::build(&vec![1.0; n]).unwrap();
let mut rng = Rng::seeded(n as u64);
let draws = 200_000;
let mut sink = 0usize;
for _ in 0..10_000 {
sink ^= table.draw(&mut rng);
}
let start = Instant::now();
for _ in 0..draws {
sink ^= table.draw(&mut rng);
}
let ns = start.elapsed().as_nanos() as f64 / draws as f64;
assert!(sink < usize::MAX, "keep the work");
ns
};
let small = per_draw(100);
let large = per_draw(1_000_000);
println!(" per draw: 100 workers {small:.1} ns, 1e6 workers {large:.1} ns");
assert!(
large < small * 20.0 + 100.0,
"a million workers must not change the shape of the cost: \
{small:.1} ns vs {large:.1} ns"
);
}
#[test]
fn building_a_million_entry_table_is_affordable_once_a_tick() {
let weights = vec![1.0; 1_000_000];
let start = Instant::now();
let table = AliasTable::build(&weights).expect("built");
let ms = start.elapsed().as_secs_f64() * 1000.0;
println!(" built 1e6 entries in {ms:.0} ms");
assert_eq!(table.len(), 1_000_000);
assert!(ms < 2_000.0, "{ms:.0} ms to build");
}
}