Skip to main content

rust_igraph/algorithms/constructors/
prufer.rs

1//! Prüfer-sequence tree decoder (ALGO-CN-016).
2//!
3//! Counterpart of `igraph_from_prufer()` in
4//! `references/igraph/src/constructors/prufer.c:55-125`.
5//!
6//! A *Prüfer sequence* is a length-`(n-2)` string over the alphabet
7//! `{0, 1, …, n-1}` that uniquely encodes a labelled tree on `n`
8//! vertices. The bijection works in both directions:
9//!
10//! * **Encoding** (not provided here — see [`igraph_to_prufer`] upstream):
11//!   repeatedly remove the smallest-labelled leaf and record its sole
12//!   neighbour's label; stop with two vertices left.
13//! * **Decoding** (this module): walk the sequence left-to-right, peel
14//!   off the smallest leaf at each step, and emit the implied edges.
15//!
16//! Implementation follows Micikevičius, Caminiti & Deo, *"Linear-time
17//! Algorithms for Encoding Trees as Sequences of Node Labels"*, which
18//! achieves `O(n)` instead of the naïve `O(n²)` priority-queue version.
19//! The trick is to track each vertex's residual degree (how many times
20//! it still appears in the unread tail of the Prüfer sequence) and
21//! cascade through leaves discovered as a side-effect of removing one.
22//!
23//! The decoder always produces an **undirected** tree on `n` vertices
24//! with exactly `n - 1` edges. The empty Prüfer sequence yields the
25//! 2-vertex path graph `P_2` (single edge `0—1`).
26//!
27//! Time complexity: `O(|V|)` where `|V| = prufer.len() + 2`.
28//!
29//! [`igraph_to_prufer`]: https://igraph.org/c/html/latest/igraph-Generators.html#igraph_to_prufer
30
31use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
32
33/// Decode a Prüfer sequence into the unique labelled tree it represents.
34///
35/// `prufer` must contain values in `[0, n)` where `n = prufer.len() + 2`.
36/// The resulting graph has exactly `n` vertices and `n - 1` undirected
37/// edges, forming a tree. The empty input slice produces the 2-vertex
38/// path graph `P_2`.
39///
40/// # Errors
41///
42/// * [`IgraphError::InvalidArgument`] — some `prufer[i]` is `>= n`,
43///   or `prufer.len() + 2` overflows [`u32`].
44///
45/// # Examples
46///
47/// ```
48/// use rust_igraph::from_prufer;
49///
50/// // Prüfer sequence [2, 3, 2, 3] encodes a 6-vertex tree.
51/// let tree = from_prufer(&[2, 3, 2, 3]).unwrap();
52/// assert_eq!(tree.vcount(), 6);
53/// assert_eq!(tree.ecount(), 5);
54/// assert!(!tree.is_directed());
55/// ```
56pub fn from_prufer(prufer: &[u32]) -> IgraphResult<Graph> {
57    let len = prufer.len();
58    let n = u32::try_from(len)
59        .ok()
60        .and_then(|l| l.checked_add(2))
61        .ok_or_else(|| {
62            IgraphError::InvalidArgument("from_prufer: prufer.len() + 2 overflows u32".to_string())
63        })?;
64
65    // n is at least 2 here (len >= 0 → n >= 2). Always emit one edge.
66    let mut degree: Vec<u32> = vec![0; n as usize];
67    for &w in prufer {
68        if w >= n {
69            return Err(IgraphError::InvalidArgument(format!(
70                "from_prufer: invalid Prufer entry {w} (must be < {n})",
71            )));
72        }
73        degree[w as usize] += 1;
74    }
75
76    let edge_count = (n - 1) as usize;
77    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(edge_count);
78
79    // Linear-time decode (Micikevičius–Caminiti–Deo). The loop walks
80    // each vertex i once; whenever the current candidate u is already a
81    // leaf (degree 0 in the unread tail), we emit its edge to the parent
82    // v read from `prufer[k]`, decrement v's degree, and try again at v.
83    let mut v: u32 = 0;
84    let mut k: usize = 0;
85    let mut last_i: u32 = 0;
86    let limit = len; // == n as usize - 2
87    for i in 0..n {
88        last_i = i;
89        let mut u = i;
90        while k < limit && u <= i && degree[u as usize] == 0 {
91            v = prufer[k];
92            edges.push((v, u));
93            k += 1;
94            degree[v as usize] -= 1;
95            u = v;
96        }
97        if k == limit {
98            break;
99        }
100    }
101
102    // Find the lone remaining leaf u > last_i, u != v.
103    let mut last_u: u32 = 0;
104    for cand in (last_i + 1)..n {
105        if degree[cand as usize] == 0 && cand != v {
106            last_u = cand;
107            break;
108        }
109    }
110    edges.push((v, last_u));
111
112    let mut g = Graph::new(n, false)?;
113    g.add_edges(edges)?;
114    Ok(g)
115}
116
117/// Encode a labelled tree into its unique Prüfer sequence.
118///
119/// The graph must be an undirected tree with at least 2 vertices.
120/// Returns a `Vec<u32>` of length `n - 2` where `n = graph.vcount()`.
121///
122/// The algorithm iteratively removes the smallest-labelled leaf and
123/// records its sole neighbour's label. Runs in `O(n)` time using the
124/// linear-time variant that avoids repeated linear scans.
125///
126/// # Errors
127///
128/// * [`IgraphError::InvalidArgument`] — graph is not a tree, or has
129///   fewer than 2 vertices.
130///
131/// # Examples
132///
133/// ```
134/// use rust_igraph::{from_prufer, to_prufer};
135///
136/// // Round-trip: encode a known tree, decode it, verify same edges.
137/// let tree = from_prufer(&[2, 3, 2, 3]).unwrap();
138/// let seq = to_prufer(&tree).unwrap();
139/// assert_eq!(seq, vec![2, 3, 2, 3]);
140/// ```
141pub fn to_prufer(graph: &Graph) -> IgraphResult<Vec<u32>> {
142    let n = graph.vcount();
143
144    if n < 2 {
145        return Err(IgraphError::InvalidArgument(
146            "to_prufer: tree must have at least 2 vertices".into(),
147        ));
148    }
149
150    // Verify it's a tree: connected + ecount == n - 1
151    let ecount = graph.ecount();
152    if ecount != (n as usize) - 1 {
153        return Err(IgraphError::InvalidArgument(
154            "to_prufer: graph is not a tree (wrong edge count)".into(),
155        ));
156    }
157
158    // Build adjacency lists
159    let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n as usize];
160    for eid in 0..ecount {
161        #[allow(clippy::cast_possible_truncation)]
162        let (from, to) = graph.edge(eid as u32)?;
163        if from == to {
164            return Err(IgraphError::InvalidArgument(
165                "to_prufer: graph contains a self-loop, not a tree".into(),
166            ));
167        }
168        adj[from as usize].push(to);
169        adj[to as usize].push(from);
170    }
171
172    // Compute degrees (safe: degree bounded by n which is u32)
173    #[allow(clippy::cast_possible_truncation)]
174    let mut degrees: Vec<u32> = adj.iter().map(|v| v.len() as u32).collect();
175
176    // Check connectivity via BFS
177    let mut bfs_visited = vec![false; n as usize];
178    let mut bfs_queue = std::collections::VecDeque::new();
179    bfs_queue.push_back(0u32);
180    bfs_visited[0] = true;
181    let mut visit_count: u32 = 1;
182    while let Some(v) = bfs_queue.pop_front() {
183        for &nbr in &adj[v as usize] {
184            if !bfs_visited[nbr as usize] {
185                bfs_visited[nbr as usize] = true;
186                visit_count += 1;
187                bfs_queue.push_back(nbr);
188            }
189        }
190    }
191    if visit_count != n {
192        return Err(IgraphError::InvalidArgument(
193            "to_prufer: graph is not connected, not a tree".into(),
194        ));
195    }
196
197    // Linear-time Prüfer encoding
198    let mut prufer: Vec<u32> = Vec::with_capacity((n - 2) as usize);
199    let mut prufer_idx: usize = 0;
200    let target_len = (n - 2) as usize;
201
202    for u in 0..n {
203        let mut leaf = u;
204        let mut deg = degrees[leaf as usize];
205
206        while deg == 1 && leaf <= u && prufer_idx < target_len {
207            degrees[leaf as usize] = 0;
208
209            // Find the unique remaining neighbor
210            let mut neighbor = 0u32;
211            for &nbr in &adj[leaf as usize] {
212                if degrees[nbr as usize] > 0 {
213                    neighbor = nbr;
214                    break;
215                }
216            }
217
218            degrees[neighbor as usize] -= 1;
219            deg = degrees[neighbor as usize];
220
221            if deg > 0 {
222                prufer.push(neighbor);
223                prufer_idx += 1;
224            }
225            leaf = neighbor;
226        }
227    }
228
229    Ok(prufer)
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    use std::collections::BTreeSet;
236
237    fn collect_edges_canonical(g: &Graph) -> BTreeSet<(VertexId, VertexId)> {
238        let m = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
239        (0..m)
240            .map(|eid| g.edge(eid).expect("edge in range"))
241            .map(|(a, b)| if a <= b { (a, b) } else { (b, a) })
242            .collect()
243    }
244
245    fn uf_find(parent: &mut [u32], mut node: u32) -> u32 {
246        while parent[node as usize] != node {
247            let grand = parent[parent[node as usize] as usize];
248            parent[node as usize] = grand;
249            node = grand;
250        }
251        node
252    }
253
254    fn is_tree(graph: &Graph) -> bool {
255        // Connected via union-find + edge count == n - 1.
256        let vcount = graph.vcount();
257        let ecount = graph.ecount();
258        if vcount == 0 {
259            return ecount == 0;
260        }
261        if ecount != (vcount as usize) - 1 {
262            return false;
263        }
264        let mut parent: Vec<u32> = (0..vcount).collect();
265        let em = u32::try_from(ecount).expect("ecount fits u32 in tests");
266        for eid in 0..em {
267            let (src, dst) = graph.edge(eid).expect("edge in range");
268            let rs = uf_find(&mut parent, src);
269            let rd = uf_find(&mut parent, dst);
270            if rs == rd {
271                return false; // cycle
272            }
273            parent[rs as usize] = rd;
274        }
275        let root = uf_find(&mut parent, 0);
276        (1..vcount).all(|v| uf_find(&mut parent, v) == root)
277    }
278
279    #[test]
280    fn empty_prufer_yields_p2() {
281        let g = from_prufer(&[]).expect("empty");
282        assert_eq!(g.vcount(), 2);
283        assert_eq!(g.ecount(), 1);
284        assert!(!g.is_directed());
285        let edges = collect_edges_canonical(&g);
286        let expected: BTreeSet<(u32, u32)> = [(0, 1)].into_iter().collect();
287        assert_eq!(edges, expected);
288    }
289
290    #[test]
291    fn upstream_fixture_2_3_2_3() {
292        // From references/igraph/tests/unit/igraph_from_prufer.out:
293        // edges: (2,0) (3,1) (4,2) (3,2) (5,3)
294        let g = from_prufer(&[2, 3, 2, 3]).expect("prufer1");
295        assert_eq!(g.vcount(), 6);
296        assert_eq!(g.ecount(), 5);
297        let edges = collect_edges_canonical(&g);
298        let expected: BTreeSet<(u32, u32)> = [(0, 2), (1, 3), (2, 4), (2, 3), (3, 5)]
299            .into_iter()
300            .collect();
301        assert_eq!(edges, expected);
302        assert!(is_tree(&g));
303    }
304
305    #[test]
306    fn upstream_fixture_0_2_4_1_1_0() {
307        // From references/igraph/tests/unit/igraph_from_prufer.out:
308        // edges: (3,0) (5,2) (4,2) (4,1) (6,1) (1,0) (7,0)
309        let g = from_prufer(&[0, 2, 4, 1, 1, 0]).expect("prufer2");
310        assert_eq!(g.vcount(), 8);
311        assert_eq!(g.ecount(), 7);
312        let edges = collect_edges_canonical(&g);
313        let expected: BTreeSet<(u32, u32)> =
314            [(0, 3), (2, 5), (2, 4), (1, 4), (1, 6), (0, 1), (0, 7)]
315                .into_iter()
316                .collect();
317        assert_eq!(edges, expected);
318        assert!(is_tree(&g));
319    }
320
321    #[test]
322    fn invalid_entry_out_of_range_errors() {
323        // n = 4, entry 4 is out of [0, 4).
324        let err = from_prufer(&[0, 4]).unwrap_err();
325        assert!(matches!(err, IgraphError::InvalidArgument(_)));
326    }
327
328    #[test]
329    fn constant_sequence_yields_star() {
330        // Repeating a single vertex u in every Prufer slot makes u the
331        // centre of a star (it is the parent of every other vertex).
332        // prufer = [0, 0, 0, 0] → n = 6, star centred on 0.
333        let g = from_prufer(&[0, 0, 0, 0]).expect("star");
334        assert_eq!(g.vcount(), 6);
335        assert_eq!(g.ecount(), 5);
336        assert!(is_tree(&g));
337        // Vertex 0 should have degree 5.
338        let deg0 = g.neighbors(0).expect("neighbors of 0").len();
339        assert_eq!(deg0, 5);
340    }
341
342    #[test]
343    fn ascending_sequence_yields_path() {
344        // prufer = [1, 2, 3, 4] → tree is the path 0-1-2-3-4-5.
345        let g = from_prufer(&[1, 2, 3, 4]).expect("path");
346        assert_eq!(g.vcount(), 6);
347        assert_eq!(g.ecount(), 5);
348        assert!(is_tree(&g));
349        // Every interior vertex has degree 2, endpoints have degree 1.
350        for v in 0..6u32 {
351            let d = g.neighbors(v).expect("neighbors").len();
352            let expected = if v == 0 || v == 5 { 1 } else { 2 };
353            assert_eq!(d, expected, "vertex {v} degree");
354        }
355    }
356
357    #[test]
358    fn single_entry_prufer() {
359        // prufer = [0] → n = 3, decode yields edges (0,1) and (0,2).
360        let g = from_prufer(&[0]).expect("single");
361        assert_eq!(g.vcount(), 3);
362        assert_eq!(g.ecount(), 2);
363        assert!(is_tree(&g));
364        let deg0 = g.neighbors(0).expect("neighbors").len();
365        assert_eq!(deg0, 2); // 0 is the centre.
366    }
367
368    #[test]
369    fn always_undirected() {
370        let g = from_prufer(&[2, 3, 2, 3]).expect("ok");
371        assert!(!g.is_directed());
372    }
373
374    #[test]
375    fn no_self_loops_or_multi_edges() {
376        let g = from_prufer(&[0, 2, 4, 1, 1, 0]).expect("ok");
377        let m = u32::try_from(g.ecount()).expect("m fits u32 in tests");
378        let mut seen: BTreeSet<(u32, u32)> = BTreeSet::new();
379        for e in 0..m {
380            let (a, b) = g.edge(e).expect("edge");
381            assert_ne!(a, b, "tree must not have self-loops");
382            let canon = if a <= b { (a, b) } else { (b, a) };
383            assert!(seen.insert(canon), "duplicate edge {canon:?}");
384        }
385    }
386
387    // --- to_prufer tests ---
388
389    #[test]
390    fn to_prufer_roundtrip_2_3_2_3() {
391        let tree = from_prufer(&[2, 3, 2, 3]).expect("decode");
392        let seq = to_prufer(&tree).expect("encode");
393        assert_eq!(seq, vec![2, 3, 2, 3]);
394    }
395
396    #[test]
397    fn to_prufer_roundtrip_constant() {
398        // Star centered at 3: Prüfer = [3, 3, 3]
399        let tree = from_prufer(&[3, 3, 3]).expect("decode");
400        let seq = to_prufer(&tree).expect("encode");
401        assert_eq!(seq, vec![3, 3, 3]);
402    }
403
404    #[test]
405    fn to_prufer_roundtrip_path() {
406        // Path 0-1-2-3: Prüfer = [1, 2]
407        let tree = from_prufer(&[1, 2]).expect("decode");
408        let seq = to_prufer(&tree).expect("encode");
409        assert_eq!(seq, vec![1, 2]);
410    }
411
412    #[test]
413    fn to_prufer_p2() {
414        // P_2 has empty Prüfer sequence
415        let mut g = Graph::with_vertices(2);
416        g.add_edge(0, 1).expect("add edge");
417        let seq = to_prufer(&g).expect("encode");
418        assert!(seq.is_empty());
419    }
420
421    #[test]
422    fn to_prufer_not_a_tree_cycle() {
423        let mut g = Graph::with_vertices(3);
424        g.add_edge(0, 1).expect("ok");
425        g.add_edge(1, 2).expect("ok");
426        g.add_edge(2, 0).expect("ok");
427        assert!(to_prufer(&g).is_err());
428    }
429
430    #[test]
431    fn to_prufer_not_a_tree_disconnected() {
432        let mut g = Graph::with_vertices(4);
433        g.add_edge(0, 1).expect("ok");
434        g.add_edge(2, 3).expect("ok");
435        // 2 edges but needs 3 for a tree on 4 vertices
436        assert!(to_prufer(&g).is_err());
437    }
438
439    #[test]
440    fn to_prufer_single_vertex() {
441        let g = Graph::with_vertices(1);
442        assert!(to_prufer(&g).is_err());
443    }
444
445    #[test]
446    fn to_prufer_roundtrip_large() {
447        let seq = vec![0, 2, 4, 1, 1, 0];
448        let tree = from_prufer(&seq).expect("decode");
449        let result = to_prufer(&tree).expect("encode");
450        assert_eq!(result, seq);
451    }
452}
453
454#[cfg(all(test, feature = "proptest-harness"))]
455mod proptest_tests {
456    use super::*;
457    use proptest::prelude::*;
458    use std::collections::BTreeSet;
459
460    fn arb_prufer() -> impl Strategy<Value = Vec<u32>> {
461        // Strategy: pick n in [2, 30], then build a length-(n-2) vector
462        // whose entries are in [0, n).
463        (2u32..30).prop_flat_map(|n| prop::collection::vec(0u32..n, (n - 2) as usize))
464    }
465
466    fn uf_find(parent: &mut [u32], mut node: u32) -> u32 {
467        while parent[node as usize] != node {
468            let grand = parent[parent[node as usize] as usize];
469            parent[node as usize] = grand;
470            node = grand;
471        }
472        node
473    }
474
475    proptest! {
476        #[test]
477        fn always_a_tree(prufer in arb_prufer()) {
478            let g = from_prufer(&prufer).unwrap();
479            let n = u32::try_from(prufer.len()).unwrap() + 2;
480            prop_assert_eq!(g.vcount(), n);
481            prop_assert_eq!(g.ecount(), (n - 1) as usize);
482            prop_assert!(!g.is_directed());
483        }
484
485        #[test]
486        fn no_self_loops(prufer in arb_prufer()) {
487            let g = from_prufer(&prufer).unwrap();
488            let m = u32::try_from(g.ecount()).unwrap();
489            for e in 0..m {
490                let (a, b) = g.edge(e).unwrap();
491                prop_assert_ne!(a, b);
492            }
493        }
494
495        #[test]
496        fn no_duplicate_edges(prufer in arb_prufer()) {
497            let g = from_prufer(&prufer).unwrap();
498            let m = u32::try_from(g.ecount()).unwrap();
499            let mut seen: BTreeSet<(u32, u32)> = BTreeSet::new();
500            for e in 0..m {
501                let (a, b) = g.edge(e).unwrap();
502                let canon = if a <= b { (a, b) } else { (b, a) };
503                prop_assert!(seen.insert(canon));
504            }
505        }
506
507        #[test]
508        fn connected_undirected_tree(prufer in arb_prufer()) {
509            // Tree iff connected and exactly n-1 edges (already checked).
510            let graph = from_prufer(&prufer).unwrap();
511            let vcount = graph.vcount();
512            let mut parent: Vec<u32> = (0..vcount).collect();
513            let ecount = u32::try_from(graph.ecount()).unwrap();
514            for eid in 0..ecount {
515                let (src, dst) = graph.edge(eid).unwrap();
516                let rs = uf_find(&mut parent, src);
517                let rd = uf_find(&mut parent, dst);
518                prop_assert_ne!(rs, rd, "cycle detected — not a tree");
519                parent[rs as usize] = rd;
520            }
521            let root = uf_find(&mut parent, 0);
522            for v in 1..vcount {
523                prop_assert_eq!(uf_find(&mut parent, v), root);
524            }
525        }
526
527        #[test]
528        fn invalid_entry_errors(
529            n in 3u32..20,
530            bad in 20u32..30,
531        ) {
532            // Construct a prufer of length n-2 where the first slot is
533            // out of range (n is at least 3, so bad in [20, 30) >> n).
534            let len = (n - 2) as usize;
535            let mut p = vec![0u32; len];
536            p[0] = bad; // bad >= 20 > n
537            let err = from_prufer(&p).unwrap_err();
538            prop_assert!(matches!(err, IgraphError::InvalidArgument(_)));
539        }
540    }
541}