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}