Skip to main content

rust_igraph/algorithms/games/
tree.rs

1//! Uniform random labelled tree generators (ALGO-GN-004).
2//!
3//! Counterpart of `igraph_tree_game()` in
4//! `references/igraph/src/games/tree.c`. Two methods are available:
5//!
6//! * **LERW** ([`tree_game_lerw`]) — Wilson's loop-erased random walk on
7//!   `K_n`. Supports both directed (out-rooted) and undirected trees.
8//! * **Prüfer** ([`tree_game_prufer`]) — samples a random Prüfer sequence,
9//!   then decodes it via [`from_prufer`]. Undirected
10//!   only, matching the C upstream restriction.
11//!
12//! Both methods produce each labelled tree with equal probability.
13//!
14//! ## LERW algorithm
15//!
16//! Wilson's loop-erased random walk on the complete graph `K_n` uniformly
17//! samples spanning trees. The igraph implementation collapses the naive
18//! "walk until you hit a visited vertex, then erase the loop" formulation
19//! into a single linear pass:
20//!
21//! 1. Maintain an array `vertices = [0, 1, …, n - 1]` partitioned so that
22//!    positions `[0, k)` hold the visited vertices and `[k, n)` hold the
23//!    unvisited ones.
24//! 2. Pick an initial vertex `i` uniformly, mark it visited, swap it to
25//!    position `0`, set `k = 1`.
26//! 3. For each remaining step `k = 1 .. n`: draw `j ∈ [0, n)`. If
27//!    `vertices[j]` is already visited (its slot lies in `[0, k)`),
28//!    advance the walk by setting `i = vertices[j]` and resample
29//!    `j ∈ [k, n)` so the next visited vertex is guaranteed to be a
30//!    fresh one. Then mark `vertices[j]` visited, swap it to position
31//!    `k`, emit the edge `(i, vertices[k])`, and update
32//!    `i = vertices[k]`.
33//!
34//! Each iteration produces exactly one edge, so the tree has `n - 1`
35//! edges. Runtime is `O(n)` walk steps amortised — there is no rejection
36//! loop because step 3a covers the "already visited" branch in one shot.
37//!
38//! ## Prüfer algorithm
39//!
40//! Generate `n - 2` uniform random values in `[0, n)`, forming a Prüfer
41//! sequence, then decode it with the linear-time algorithm from
42//! [`from_prufer`]. The result is always an undirected
43//! labelled tree.
44
45#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
46
47use crate::algorithms::constructors::prufer::from_prufer;
48use crate::core::rng::SplitMix64;
49use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
50
51/// Generate a uniformly random labelled tree on `n` vertices using
52/// Wilson's loop-erased random walk.
53///
54/// * `n` — vertex count. `n = 0` returns an empty graph, `n = 1`
55///   returns a single isolated vertex.
56/// * `directed` — if `true`, edges are stored with parent-to-child
57///   orientation in walk order (the resulting tree is rooted at the
58///   randomly chosen initial vertex). If `false`, the same edges are
59///   stored as an undirected tree.
60/// * `seed` — initialises an internal `SplitMix64` PRNG. Same
61///   `(n, directed, seed)` always yields the same tree.
62///
63/// The output has exactly `max(0, n - 1)` edges, is acyclic, has no
64/// self-loops, and (in the undirected case) is connected.
65///
66/// # Examples
67///
68/// ```
69/// use rust_igraph::tree_game_lerw;
70///
71/// // 30-vertex undirected uniform random tree.
72/// let g = tree_game_lerw(30, false, 0xC0FF_EE00).unwrap();
73/// assert_eq!(g.vcount(), 30);
74/// assert_eq!(g.ecount(), 29);  // n - 1 edges
75/// assert!(!g.is_directed());
76/// ```
77pub fn tree_game_lerw(n: u32, directed: bool, seed: u64) -> IgraphResult<Graph> {
78    if n < 2 {
79        return Graph::new(n, directed);
80    }
81
82    let n_usize = n as usize;
83    let no_edges = n_usize - 1;
84
85    let mut vertices: Vec<VertexId> = (0..n).collect();
86    let mut visited: Vec<bool> = vec![false; n_usize];
87    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(no_edges);
88
89    let mut rng = SplitMix64::new(seed);
90
91    // Pick the initial vertex uniformly, mark it visited, swap to slot 0.
92    let i0 = rng.gen_index(n_usize);
93    visited[vertices[i0] as usize] = true;
94    vertices.swap(0, i0);
95
96    let mut prev: VertexId = vertices[0];
97
98    for k in 1..n_usize {
99        // Draw a candidate slot from [0, n).
100        let mut j = rng.gen_index(n_usize);
101        if visited[vertices[j] as usize] {
102            // Already visited — advance the walk and resample from the
103            // unvisited tail [k, n) so the next emit is fresh.
104            prev = vertices[j];
105            j = k + rng.gen_index(n_usize - k);
106        }
107        visited[vertices[j] as usize] = true;
108        vertices.swap(k, j);
109        let new_v = vertices[k];
110        edges.push((prev, new_v));
111        prev = new_v;
112    }
113
114    let mut g = Graph::new(n, directed)?;
115    g.add_edges(edges)?;
116    Ok(g)
117}
118
119/// Generate a uniformly random labelled tree on `n` vertices using the
120/// Prüfer sequence method.
121///
122/// Samples a random Prüfer sequence of length `n - 2` (each entry
123/// uniform in `[0, n)`), then decodes it with [`from_prufer`] to
124/// produce an undirected labelled tree.
125///
126/// * `n` — vertex count. `n = 0` returns an empty graph, `n = 1`
127///   returns a single isolated vertex.
128/// * `seed` — initialises an internal `SplitMix64` PRNG. Same
129///   `(n, seed)` always yields the same tree.
130///
131/// The output has exactly `max(0, n - 1)` edges, is acyclic, has no
132/// self-loops, and is connected.
133///
134/// Counterpart of the `IGRAPH_RANDOM_TREE_PRUFER` branch of
135/// `igraph_tree_game()` in `references/igraph/src/games/tree.c:37-56`.
136///
137/// # Errors
138///
139/// * [`IgraphError::InvalidArgument`] — `n` overflows internal
140///   allocation limits.
141///
142/// # Examples
143///
144/// ```
145/// use rust_igraph::tree_game_prufer;
146///
147/// let g = tree_game_prufer(30, 0xC0FF_EE00).unwrap();
148/// assert_eq!(g.vcount(), 30);
149/// assert_eq!(g.ecount(), 29);
150/// assert!(!g.is_directed());
151/// ```
152pub fn tree_game_prufer(n: u32, seed: u64) -> IgraphResult<Graph> {
153    if n < 2 {
154        return Graph::new(n, false);
155    }
156
157    let seq_len = (n - 2) as usize;
158    let mut rng = SplitMix64::new(seed);
159    let n_usize = n as usize;
160
161    let mut prufer: Vec<u32> = Vec::with_capacity(seq_len);
162    for _ in 0..seq_len {
163        let val = rng.gen_index(n_usize);
164        let val_u32 = u32::try_from(val).map_err(|_| {
165            IgraphError::InvalidArgument("tree_game_prufer: vertex index overflow".to_string())
166        })?;
167        prufer.push(val_u32);
168    }
169
170    from_prufer(&prufer)
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    fn collect_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
178        let n_edges = u32::try_from(g.ecount()).expect("ecount fits in u32 in tests");
179        (0..n_edges)
180            .map(|eid| g.edge(eid).expect("edge id in bounds"))
181            .collect()
182    }
183
184    /// Union-find for connectivity / acyclicity invariants.
185    struct UnionFind {
186        parent: Vec<u32>,
187    }
188    impl UnionFind {
189        fn new(n: usize) -> Self {
190            Self {
191                parent: (0..n as u32).collect(),
192            }
193        }
194        fn find(&mut self, mut x: u32) -> u32 {
195            while self.parent[x as usize] != x {
196                let p = self.parent[x as usize];
197                self.parent[x as usize] = self.parent[p as usize];
198                x = self.parent[x as usize];
199            }
200            x
201        }
202        /// Returns `true` when the union created a new bridge,
203        /// `false` if the two ends were already in the same component.
204        fn union(&mut self, a: u32, b: u32) -> bool {
205            let ra = self.find(a);
206            let rb = self.find(b);
207            if ra == rb {
208                false
209            } else {
210                self.parent[ra as usize] = rb;
211                true
212            }
213        }
214    }
215
216    #[test]
217    fn n_zero_returns_empty_graph() {
218        let g = tree_game_lerw(0, false, 1).unwrap();
219        assert_eq!(g.vcount(), 0);
220        assert_eq!(g.ecount(), 0);
221    }
222
223    #[test]
224    fn n_one_returns_singleton() {
225        let g = tree_game_lerw(1, false, 1).unwrap();
226        assert_eq!(g.vcount(), 1);
227        assert_eq!(g.ecount(), 0);
228    }
229
230    #[test]
231    fn n_two_returns_single_edge() {
232        let g = tree_game_lerw(2, false, 0xBEEF).unwrap();
233        assert_eq!(g.vcount(), 2);
234        assert_eq!(g.ecount(), 1);
235        let (a, b) = g.edge(0).unwrap();
236        assert_ne!(a, b, "tree edge must not be a self-loop");
237        let lo = a.min(b);
238        let hi = a.max(b);
239        assert_eq!(lo, 0);
240        assert_eq!(hi, 1);
241    }
242
243    #[test]
244    fn exact_edge_count() {
245        for &n in &[5u32, 10, 50, 200, 1000] {
246            let g = tree_game_lerw(n, false, 0xC0FF_EE00 + u64::from(n)).unwrap();
247            assert_eq!(g.vcount(), n);
248            assert_eq!(g.ecount() as u32, n - 1);
249        }
250    }
251
252    #[test]
253    fn no_self_loops() {
254        let g = tree_game_lerw(150, false, 0xFACE).unwrap();
255        for (a, b) in collect_edges(&g) {
256            assert_ne!(a, b, "Wilson LERW must not emit self-loops");
257        }
258    }
259
260    #[test]
261    fn output_is_acyclic_and_connected_undirected() {
262        let g = tree_game_lerw(200, false, 0xCAFE).unwrap();
263        let n = g.vcount() as usize;
264        let mut uf = UnionFind::new(n);
265        for (a, b) in collect_edges(&g) {
266            assert!(
267                uf.union(a, b),
268                "edge ({a}, {b}) closed a cycle — output is not a tree"
269            );
270        }
271        // After n - 1 distinct unions all vertices must share one root.
272        let root = uf.find(0);
273        for v in 1..n as u32 {
274            assert_eq!(
275                uf.find(v),
276                root,
277                "vertex {v} is in a disconnected component"
278            );
279        }
280    }
281
282    #[test]
283    fn output_is_acyclic_and_connected_directed() {
284        // Directed mode stores edges with parent→child orientation, but
285        // connectivity should still hold on the underlying undirected
286        // graph.
287        let g = tree_game_lerw(150, true, 0xDEAD).unwrap();
288        assert!(g.is_directed());
289        let n = g.vcount() as usize;
290        let mut uf = UnionFind::new(n);
291        for (a, b) in collect_edges(&g) {
292            assert!(
293                uf.union(a, b),
294                "directed tree edge ({a}, {b}) closed a cycle"
295            );
296        }
297        let root = uf.find(0);
298        for v in 1..n as u32 {
299            assert_eq!(uf.find(v), root);
300        }
301    }
302
303    #[test]
304    fn directed_mode_has_no_duplicate_targets() {
305        // Each freshly visited vertex appears exactly once as a target,
306        // which means in-degree of every non-root vertex is exactly 1.
307        let g = tree_game_lerw(100, true, 0xBAD_F00D).unwrap();
308        let n = g.vcount() as usize;
309        let mut indeg = vec![0u32; n];
310        for (_a, b) in collect_edges(&g) {
311            indeg[b as usize] += 1;
312        }
313        let roots: Vec<_> = indeg.iter().enumerate().filter(|&(_, &d)| d == 0).collect();
314        assert_eq!(roots.len(), 1, "directed Wilson tree has exactly one root");
315        for (v, &d) in indeg.iter().enumerate() {
316            if v == roots[0].0 {
317                continue;
318            }
319            assert_eq!(d, 1, "non-root vertex {v} must have in-degree 1, got {d}");
320        }
321    }
322
323    #[test]
324    fn deterministic_with_seed() {
325        let a = tree_game_lerw(80, false, 0xABCD).unwrap();
326        let b = tree_game_lerw(80, false, 0xABCD).unwrap();
327        assert_eq!(a.vcount(), b.vcount());
328        assert_eq!(a.ecount(), b.ecount());
329        assert_eq!(collect_edges(&a), collect_edges(&b));
330    }
331
332    #[test]
333    fn different_seeds_yield_different_trees() {
334        let a = tree_game_lerw(60, false, 1).unwrap();
335        let b = tree_game_lerw(60, false, 2).unwrap();
336        assert_ne!(
337            collect_edges(&a),
338            collect_edges(&b),
339            "different seeds must produce different trees"
340        );
341    }
342
343    #[test]
344    fn all_vertices_appear_in_some_edge_for_n_ge_2() {
345        // A spanning tree touches every vertex.
346        let g = tree_game_lerw(40, false, 0x5EED).unwrap();
347        let mut seen = vec![false; g.vcount() as usize];
348        for (a, b) in collect_edges(&g) {
349            seen[a as usize] = true;
350            seen[b as usize] = true;
351        }
352        for (v, &s) in seen.iter().enumerate() {
353            assert!(s, "vertex {v} missing from spanning tree");
354        }
355    }
356
357    // ── tree_game_prufer tests ──────────────────────────────────────
358
359    #[test]
360    fn prufer_n_zero() {
361        let g = tree_game_prufer(0, 1).unwrap();
362        assert_eq!(g.vcount(), 0);
363        assert_eq!(g.ecount(), 0);
364    }
365
366    #[test]
367    fn prufer_n_one() {
368        let g = tree_game_prufer(1, 1).unwrap();
369        assert_eq!(g.vcount(), 1);
370        assert_eq!(g.ecount(), 0);
371    }
372
373    #[test]
374    fn prufer_n_two() {
375        let g = tree_game_prufer(2, 0xBEEF).unwrap();
376        assert_eq!(g.vcount(), 2);
377        assert_eq!(g.ecount(), 1);
378        assert!(!g.is_directed());
379    }
380
381    #[test]
382    fn prufer_always_undirected() {
383        let g = tree_game_prufer(50, 0xFACE).unwrap();
384        assert!(!g.is_directed());
385    }
386
387    #[test]
388    fn prufer_exact_edge_count() {
389        for &n in &[5u32, 10, 50, 200, 1000] {
390            let g = tree_game_prufer(n, 0xC0DE + u64::from(n)).unwrap();
391            assert_eq!(g.vcount(), n);
392            assert_eq!(g.ecount() as u32, n - 1);
393        }
394    }
395
396    #[test]
397    fn prufer_no_self_loops() {
398        let g = tree_game_prufer(150, 0xFACE).unwrap();
399        for (a, b) in collect_edges(&g) {
400            assert_ne!(a, b, "Prüfer tree must not emit self-loops");
401        }
402    }
403
404    #[test]
405    fn prufer_is_acyclic_and_connected() {
406        let g = tree_game_prufer(200, 0xCAFE).unwrap();
407        let n = g.vcount() as usize;
408        let mut uf = UnionFind::new(n);
409        for (a, b) in collect_edges(&g) {
410            assert!(
411                uf.union(a, b),
412                "edge ({a}, {b}) closed a cycle — not a tree"
413            );
414        }
415        let root = uf.find(0);
416        for v in 1..n as u32 {
417            assert_eq!(uf.find(v), root, "vertex {v} in disconnected component");
418        }
419    }
420
421    #[test]
422    fn prufer_deterministic_with_seed() {
423        let a = tree_game_prufer(80, 0xABCD).unwrap();
424        let b = tree_game_prufer(80, 0xABCD).unwrap();
425        assert_eq!(collect_edges(&a), collect_edges(&b));
426    }
427
428    #[test]
429    fn prufer_different_seeds_yield_different_trees() {
430        let a = tree_game_prufer(60, 1).unwrap();
431        let b = tree_game_prufer(60, 2).unwrap();
432        assert_ne!(
433            collect_edges(&a),
434            collect_edges(&b),
435            "different seeds must produce different trees"
436        );
437    }
438
439    #[test]
440    fn prufer_all_vertices_touched() {
441        let g = tree_game_prufer(40, 0x5EED).unwrap();
442        let mut seen = vec![false; g.vcount() as usize];
443        for (a, b) in collect_edges(&g) {
444            seen[a as usize] = true;
445            seen[b as usize] = true;
446        }
447        for (v, &s) in seen.iter().enumerate() {
448            assert!(s, "vertex {v} missing from spanning tree");
449        }
450    }
451}