Skip to main content

rust_igraph/algorithms/constructors/
kary_tree.rs

1//! k-ary tree constructor (ALGO-CN-004).
2//!
3//! Counterpart of `igraph_kary_tree()` in
4//! `references/igraph/src/constructors/regular.c:608-706`.
5//!
6//! A *k-ary tree* on `n` vertices is the rooted tree obtained by
7//! attaching children to parents in breadth-first order: vertex `0` is
8//! the root, vertex `1..=children` are its kids, then vertex `1`'s
9//! kids, then vertex `2`'s kids, and so on. Almost every internal
10//! vertex has exactly `children` children — the last internal vertex
11//! may have fewer if `n - 1` is not a multiple of `children`. Arc
12//! direction is controlled by [`TreeMode`]:
13//!
14//! * [`TreeMode::Out`] — directed, every edge flows **from parent to
15//!   child** (`parent → child`).
16//! * [`TreeMode::In`] — directed, every edge flows **from child to
17//!   parent** (`child → parent`).
18//! * [`TreeMode::Undirected`] — undirected; raw endpoints are
19//!   `(parent, child)` but stored as canonical `(min, max)` by
20//!   `Graph::add_edges`.
21//!
22//! Edge enumeration mirrors the upstream C loop exactly: a `to`
23//! pointer starts at `1` and advances once per emitted edge; an outer
24//! `parent` pointer starts at `0` and increments after every batch of
25//! `children` edges (or when `n - 1` edges have been emitted in
26//! total). For `n - 1` edges total, the result is always a tree
27//! (acyclic, connected when undirected, weakly connected otherwise).
28//!
29//! Edge counts:
30//!
31//! * `n = 0` — no edges (empty graph).
32//! * `n = 1` — no edges (singleton root).
33//! * `n ≥ 2`, all modes — `n - 1` edges.
34//!
35//! Time complexity: O(|V|).
36
37use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
38
39/// Direction policy for [`kary_tree`].
40///
41/// Mirrors `igraph_tree_mode_t` in the C reference.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub enum TreeMode {
44    /// Directed tree, arcs flow from parent to child.
45    Out,
46    /// Directed tree, arcs flow from child to parent.
47    In,
48    /// Undirected tree.
49    Undirected,
50}
51
52impl TreeMode {
53    fn is_directed(self) -> bool {
54        !matches!(self, TreeMode::Undirected)
55    }
56}
57
58/// Build a k-ary tree on `n` vertices where each non-leaf parent has
59/// up to `children` kids attached in BFS order.
60///
61/// For a perfectly symmetric tree of depth `l` use
62/// `n = (children^(l+1) - 1) / (children - 1)`.
63///
64/// See the module-level docs for the precise BFS layout and per-mode
65/// arc orientation.
66///
67/// # Errors
68///
69/// * [`IgraphError::InvalidArgument`] — `children == 0`.
70///
71/// # Examples
72///
73/// ```
74/// use rust_igraph::{kary_tree, TreeMode};
75///
76/// // Binary tree with 7 vertices (perfect depth-2 tree).
77/// let t = kary_tree(7, 2, TreeMode::Undirected).unwrap();
78/// assert_eq!(t.vcount(), 7);
79/// assert_eq!(t.ecount(), 6); // n - 1 edges
80/// assert!(!t.is_directed());
81/// ```
82pub fn kary_tree(n: u32, children: u32, mode: TreeMode) -> IgraphResult<Graph> {
83    if children == 0 {
84        return Err(IgraphError::InvalidArgument(
85            "kary_tree: number of children must be positive".to_string(),
86        ));
87    }
88
89    let directed = mode.is_directed();
90
91    if n == 0 {
92        return Graph::new(0, directed);
93    }
94    if n == 1 {
95        return Graph::new(1, directed);
96    }
97
98    let edge_count = (n as usize) - 1;
99    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(edge_count);
100
101    // Upstream uses an `idx < 2 * (n - 1)` flat-array termination; we
102    // bound on emitted edge count instead. `to` pumps the next child
103    // index, `parent` walks the BFS parent ordering. Inner loop emits
104    // up to `children` edges then advances `parent`.
105    let mut to: u32 = 1;
106    let mut parent: u32 = 0;
107    let mut emitted: u32 = 0;
108    let remaining = n - 1;
109    while emitted < remaining {
110        let batch = children.min(remaining - emitted);
111        for _ in 0..batch {
112            match mode {
113                TreeMode::Out | TreeMode::Undirected => edges.push((parent, to)),
114                TreeMode::In => edges.push((to, parent)),
115            }
116            to += 1;
117            emitted += 1;
118        }
119        parent += 1;
120    }
121
122    let mut g = Graph::new(n, directed)?;
123    g.add_edges(edges)?;
124    Ok(g)
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    fn collect_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
132        let m = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
133        (0..m)
134            .map(|eid| g.edge(eid).expect("edge id in bounds"))
135            .collect()
136    }
137
138    #[test]
139    fn empty_graph_all_modes() {
140        for mode in [TreeMode::Out, TreeMode::In, TreeMode::Undirected] {
141            let g = kary_tree(0, 2, mode).expect("tree n=0");
142            assert_eq!(g.vcount(), 0);
143            assert_eq!(g.ecount(), 0);
144            assert_eq!(g.is_directed(), mode.is_directed());
145        }
146    }
147
148    #[test]
149    fn singleton_has_no_edges() {
150        for mode in [TreeMode::Out, TreeMode::In, TreeMode::Undirected] {
151            let g = kary_tree(1, 3, mode).expect("tree n=1");
152            assert_eq!(g.vcount(), 1);
153            assert_eq!(g.ecount(), 0);
154        }
155    }
156
157    #[test]
158    fn binary_tree_seven_vertices_out_mode() {
159        // n=7, children=2: perfect binary tree depth 2.
160        //   parent 0 -> 1, 2
161        //   parent 1 -> 3, 4
162        //   parent 2 -> 5, 6
163        let g = kary_tree(7, 2, TreeMode::Out).expect("B7");
164        assert!(g.is_directed());
165        assert_eq!(g.ecount(), 6);
166        assert_eq!(
167            collect_edges(&g),
168            vec![(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)]
169        );
170    }
171
172    #[test]
173    fn binary_tree_seven_vertices_in_mode() {
174        // Same shape, edge direction reversed: every (parent, child) becomes
175        // (child, parent).
176        let g = kary_tree(7, 2, TreeMode::In).expect("B7 in");
177        assert!(g.is_directed());
178        assert_eq!(g.ecount(), 6);
179        assert_eq!(
180            collect_edges(&g),
181            vec![(1, 0), (2, 0), (3, 1), (4, 1), (5, 2), (6, 2)]
182        );
183    }
184
185    #[test]
186    fn binary_tree_seven_vertices_undirected_mode() {
187        // Same raw edges as OUT, canonicalised by add_edges (parent<child
188        // already so no swap occurs).
189        let g = kary_tree(7, 2, TreeMode::Undirected).expect("B7 undir");
190        assert!(!g.is_directed());
191        assert_eq!(g.ecount(), 6);
192        assert_eq!(
193            collect_edges(&g),
194            vec![(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)]
195        );
196    }
197
198    #[test]
199    fn ternary_tree_eight_vertices_partial_last_parent() {
200        // n=8, children=3:
201        //   parent 0 -> 1, 2, 3
202        //   parent 1 -> 4, 5, 6
203        //   parent 2 -> 7 (only one child — emission stops at n-1=7 edges)
204        let g = kary_tree(8, 3, TreeMode::Out).expect("T8");
205        assert_eq!(g.ecount(), 7);
206        assert_eq!(
207            collect_edges(&g),
208            vec![(0, 1), (0, 2), (0, 3), (1, 4), (1, 5), (1, 6), (2, 7)]
209        );
210    }
211
212    #[test]
213    fn linear_chain_one_child_per_parent() {
214        // children=1 → path graph 0-1-2-3-4-5.
215        let g = kary_tree(6, 1, TreeMode::Out).expect("path-6");
216        assert_eq!(g.ecount(), 5);
217        assert_eq!(
218            collect_edges(&g),
219            vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]
220        );
221    }
222
223    #[test]
224    fn star_when_children_at_least_n_minus_one() {
225        // children >= n-1 → first parent gets all the rest as kids: a
226        // star (root at vertex 0).
227        let g = kary_tree(5, 10, TreeMode::Out).expect("star-5 via tree");
228        assert_eq!(g.ecount(), 4);
229        assert_eq!(collect_edges(&g), vec![(0, 1), (0, 2), (0, 3), (0, 4)]);
230    }
231
232    #[test]
233    fn zero_children_errors() {
234        let err = kary_tree(5, 0, TreeMode::Out).unwrap_err();
235        assert!(matches!(err, IgraphError::InvalidArgument(_)));
236    }
237
238    #[test]
239    fn binary_tree_is_acyclic_and_connected() {
240        // For n=15 binary (perfect depth 3), check tree topology:
241        // exactly n-1 edges and every non-root has exactly one parent.
242        let g = kary_tree(15, 2, TreeMode::Undirected).expect("B15");
243        assert_eq!(g.ecount(), 14);
244        // Every non-root vertex appears exactly once as a child in the
245        // upstream BFS layout.
246        for v in 1..15u32 {
247            let degree = g.neighbors(v).expect("neighbors").len();
248            // Leaf or internal: every non-root vertex has at least its
249            // parent edge; internal vertices add their children's edges
250            // on top.
251            assert!(degree >= 1, "vertex {v} disconnected");
252        }
253    }
254
255    #[test]
256    fn child_count_pattern_for_n_seven_three() {
257        // n=7, children=3:
258        //   parent 0 -> 1, 2, 3
259        //   parent 1 -> 4, 5, 6
260        // parent 2 onward gets no kids (already 6 edges emitted = n-1).
261        let g = kary_tree(7, 3, TreeMode::Out).expect("T7");
262        assert_eq!(g.ecount(), 6);
263        assert_eq!(
264            collect_edges(&g),
265            vec![(0, 1), (0, 2), (0, 3), (1, 4), (1, 5), (1, 6)]
266        );
267    }
268}
269
270#[cfg(all(test, feature = "proptest-harness"))]
271mod proptest_tests {
272    use super::*;
273    use proptest::prelude::*;
274
275    fn arb_mode() -> impl Strategy<Value = TreeMode> {
276        prop_oneof![
277            Just(TreeMode::Out),
278            Just(TreeMode::In),
279            Just(TreeMode::Undirected),
280        ]
281    }
282
283    proptest! {
284        #[test]
285        fn edge_count_is_n_minus_one(
286            n in 0u32..50,
287            children in 1u32..10,
288            mode in arb_mode(),
289        ) {
290            let g = kary_tree(n, children, mode).unwrap();
291            prop_assert_eq!(g.vcount(), n);
292            prop_assert_eq!(g.is_directed(), mode.is_directed());
293            let expected = n.saturating_sub(1);
294            let m = u32::try_from(g.ecount()).unwrap();
295            prop_assert_eq!(m, expected);
296        }
297
298        #[test]
299        fn children_zero_always_errors(
300            n in 0u32..30,
301            mode in arb_mode(),
302        ) {
303            prop_assert!(kary_tree(n, 0, mode).is_err());
304        }
305
306        #[test]
307        fn every_non_root_appears_exactly_once_as_child(
308            n in 2u32..30,
309            children in 1u32..6,
310            mode in arb_mode(),
311        ) {
312            let g = kary_tree(n, children, mode).unwrap();
313            let m = u32::try_from(g.ecount()).unwrap();
314            // Collect the "child" endpoint of every edge (the endpoint
315            // that the BFS layout assigned as a child — the higher-id
316            // endpoint after canonicalisation for undirected).
317            let mut child_count = vec![0u32; n as usize];
318            for e in 0..m {
319                let (u, v) = g.edge(e).unwrap();
320                let child = match mode {
321                    TreeMode::Out | TreeMode::Undirected => v,
322                    TreeMode::In => u,
323                };
324                child_count[child as usize] += 1;
325            }
326            // Vertex 0 is the root — never a child.
327            prop_assert_eq!(child_count[0], 0);
328            // Every other vertex is a child exactly once.
329            for v in 1..n {
330                prop_assert_eq!(child_count[v as usize], 1);
331            }
332        }
333    }
334}