Skip to main content

ftui_widgets/
louds.rs

1//! LOUDS (Level-Order Unary Degree Sequence) tree encoding.
2//!
3//! Compacts a tree structure from O(n × ptr_size) to 2n+1 bits while
4//! supporting O(1) parent, first_child, next_sibling, and is_leaf
5//! navigation via rank/select on a bitvector.
6//!
7//! # Encoding
8//!
9//! Traverse the tree in level order (BFS). For each node with `d` children,
10//! emit `d` one-bits followed by a zero-bit. Prepend a sentinel `10` for
11//! the super-root. Total bits: `2n + 1` for `n` nodes.
12//!
13//! # Navigation
14//!
15//! All navigation is O(1) via rank/select on the bitvector:
16//! - `parent(v)`: `select1(rank0(v) - 1)`
17//! - `first_child(v)`: `select0(rank1(v)) + 1`
18//! - `next_sibling(v)`: `v + 1` (if bit at `v + 1` is `1`)
19//! - `is_leaf(v)`: bit at `first_child(v)` is `0` or past end
20//!
21//! # Example
22//!
23//! ```
24//! use ftui_widgets::louds::LoudsTree;
25//!
26//! // Build a tree:
27//! //       root (0)
28//! //      /    \
29//! //    a (1)   b (2)
30//! //    |
31//! //    c (3)
32//! let louds = LoudsTree::from_degrees(&[2, 1, 0, 0]);
33//!
34//! assert_eq!(louds.node_count(), 4);
35//! assert_eq!(louds.first_child(0), Some(1));
36//! assert_eq!(louds.next_sibling(1), Some(2));
37//! assert_eq!(louds.first_child(1), Some(3));
38//! assert!(louds.is_leaf(2));
39//! assert!(louds.is_leaf(3));
40//! assert_eq!(louds.parent(1), Some(0));
41//! assert_eq!(louds.parent(3), Some(1));
42//! ```
43
44/// Number of `u64` words per rank superblock (512 bits).
45const SUPERBLOCK_WORDS: usize = 8;
46
47/// LOUDS-encoded tree with O(1) navigation.
48///
49/// Stores the tree structure in 2n+1 bits plus rank superblocks for fast
50/// rank/select queries.
51#[derive(Debug, Clone)]
52pub struct LoudsTree {
53    /// The LOUDS bitvector (including super-root sentinel).
54    bits: Vec<u64>,
55    /// Cumulative popcount at superblock boundaries.
56    rank_superblocks: Vec<u64>,
57    /// Total number of bits in the bitvector.
58    bit_len: usize,
59    /// Number of tree nodes (excluding the super-root).
60    n: usize,
61}
62
63impl LoudsTree {
64    /// Build a LOUDS tree from BFS-order degree sequence.
65    ///
66    /// `degrees[i]` is the number of children of the `i`-th node in
67    /// level-order. The root is `degrees[0]`.
68    ///
69    /// # Panics
70    ///
71    /// Panics if the degree sequence is empty or inconsistent (doesn't
72    /// describe a valid tree).
73    pub fn from_degrees(degrees: &[usize]) -> Self {
74        assert!(!degrees.is_empty(), "degree sequence must not be empty");
75
76        let n = degrees.len();
77        // Total bits: super-root (1,0) + for each node d_i ones + one zero = 2 + sum(d_i) + n
78        // Since sum(d_i) = n - 1 for a tree: total = 2 + (n - 1) + n = 2n + 1
79        let total_children: usize = degrees.iter().sum();
80        assert_eq!(
81            total_children,
82            n - 1,
83            "degree sum ({total_children}) must equal n-1 ({}) for a tree with {n} nodes",
84            n - 1
85        );
86
87        let bit_len = 2 * n + 1;
88        let num_words = bit_len.div_ceil(64);
89        let mut bits = vec![0u64; num_words];
90
91        // Sentinel super-root: bit 0 = 1, bit 1 = 0
92        set_bit(&mut bits, 0);
93        // bit 1 is already 0
94
95        let mut pos = 2; // Start after sentinel "10"
96        for &d in degrees {
97            for _ in 0..d {
98                set_bit(&mut bits, pos);
99                pos += 1;
100            }
101            // Zero bit (separator) — already 0
102            pos += 1;
103        }
104
105        assert_eq!(
106            pos, bit_len,
107            "encoding used {pos} bits but expected {bit_len}"
108        );
109
110        let rank_superblocks = build_rank_superblocks(&bits);
111
112        Self {
113            bits,
114            rank_superblocks,
115            bit_len,
116            n,
117        }
118    }
119
120    /// Build a LOUDS tree from a pointer-based tree (children list per node).
121    ///
122    /// `children[i]` is a slice of child node indices for node `i`. Nodes
123    /// must be numbered `0..n` in BFS order.
124    ///
125    /// # Panics
126    ///
127    /// Panics if the tree structure is inconsistent.
128    pub fn from_children(children: &[&[usize]]) -> Self {
129        let degrees: Vec<usize> = children.iter().map(|c| c.len()).collect();
130        Self::from_degrees(&degrees)
131    }
132
133    /// Number of nodes in the tree.
134    #[inline]
135    pub fn node_count(&self) -> usize {
136        self.n
137    }
138
139    /// Whether the tree is empty (no nodes).
140    #[inline]
141    pub fn is_empty(&self) -> bool {
142        self.n == 0
143    }
144
145    /// Total memory usage in bytes (excluding struct overhead).
146    pub fn size_in_bytes(&self) -> usize {
147        self.bits.len() * 8 + self.rank_superblocks.len() * 8
148    }
149
150    /// Degree (number of children) of node `v`.
151    ///
152    /// # Panics
153    ///
154    /// Panics if `v >= node_count()`.
155    pub fn degree(&self, v: usize) -> usize {
156        assert!(v < self.n, "node {v} out of bounds (n={})", self.n);
157        // Node v's block ends at its 0-bit (select0(v+1)) and starts after
158        // the previous node's 0-bit.  The degree is the number of 1-bits in
159        // that block, which equals end - start.
160        let end = self.select0(v + 1); // node v's terminating 0-bit
161        let start = if v == 0 {
162            self.select0(0) + 1 // after super-root sentinel "10"
163        } else {
164            self.select0(v) + 1 // after previous node's 0-bit
165        };
166        end - start
167    }
168
169    /// Parent of node `v`, or `None` for the root (node 0).
170    ///
171    /// # Panics
172    ///
173    /// Panics if `v >= node_count()`.
174    pub fn parent(&self, v: usize) -> Option<usize> {
175        assert!(v < self.n, "node {v} out of bounds (n={})", self.n);
176        if v == 0 {
177            return None;
178        }
179        // Node v (for v > 0) has a 1-bit in its parent's degree block.
180        // The 0th 1-bit is the super-root sentinel, so node v's 1-bit is
181        // the v-th 1-bit (0-indexed).
182        let child_bit = self.select1(v);
183        // rank0 counts 0-bits before this position. Subtract 1 for the
184        // super-root's 0-bit to get the parent's BFS node index.
185        Some(self.rank0(child_bit) - 1)
186    }
187
188    /// First child of node `v`, or `None` if `v` is a leaf.
189    ///
190    /// # Panics
191    ///
192    /// Panics if `v >= node_count()`.
193    pub fn first_child(&self, v: usize) -> Option<usize> {
194        assert!(v < self.n, "node {v} out of bounds (n={})", self.n);
195
196        // Node v's degree block starts at select0(v) + 1 (after the v-th 0-bit).
197        // If the first bit is 0, the node is a leaf.
198        let block_start = self.select0(v) + 1;
199        if block_start >= self.bit_len || !get_bit(&self.bits, block_start) {
200            return None;
201        }
202
203        // The 1-bit AT block_start represents the first child.
204        // rank1(block_start + 1) counts 1-bits in [0, block_start+1), including this one.
205        // Subtract 1 for the super-root sentinel's 1-bit.
206        Some(self.rank1(block_start + 1) - 1)
207    }
208
209    /// Next sibling of node `v`, or `None` if `v` is the last child.
210    ///
211    /// # Panics
212    ///
213    /// Panics if `v >= node_count()`.
214    pub fn next_sibling(&self, v: usize) -> Option<usize> {
215        assert!(v < self.n, "node {v} out of bounds (n={})", self.n);
216        if v == 0 {
217            return None; // root has no siblings
218        }
219
220        // Node v's 1-bit is at select1(v). The next bit is either another
221        // 1-bit (next sibling) or a 0-bit (end of parent's degree block).
222        let v_bit = self.select1(v);
223        let next_bit = v_bit + 1;
224        if next_bit >= self.bit_len || !get_bit(&self.bits, next_bit) {
225            return None;
226        }
227        Some(v + 1)
228    }
229
230    /// Whether node `v` is a leaf (has no children).
231    ///
232    /// # Panics
233    ///
234    /// Panics if `v >= node_count()`.
235    pub fn is_leaf(&self, v: usize) -> bool {
236        self.first_child(v).is_none()
237    }
238
239    /// Depth of node `v` (root has depth 0).
240    ///
241    /// This is O(depth) — it walks to the root via `parent()`.
242    ///
243    /// # Panics
244    ///
245    /// Panics if `v >= node_count()`.
246    pub fn depth(&self, v: usize) -> usize {
247        let mut d = 0;
248        let mut cur = v;
249        while let Some(p) = self.parent(cur) {
250            d += 1;
251            cur = p;
252        }
253        d
254    }
255
256    /// Iterator over children of node `v`.
257    ///
258    /// # Panics
259    ///
260    /// Panics if `v >= node_count()`.
261    pub fn children(&self, v: usize) -> ChildIter<'_> {
262        assert!(v < self.n, "node {v} out of bounds (n={})", self.n);
263        let first = self.first_child(v);
264        ChildIter {
265            tree: self,
266            next: first,
267        }
268    }
269
270    /// Subtree size rooted at node `v` (including `v` itself).
271    ///
272    /// This is O(subtree_size) — it performs a BFS within the subtree.
273    ///
274    /// # Panics
275    ///
276    /// Panics if `v >= node_count()`.
277    pub fn subtree_size(&self, v: usize) -> usize {
278        assert!(v < self.n, "node {v} out of bounds (n={})", self.n);
279        let mut count = 0;
280        let mut queue = vec![v];
281        while let Some(node) = queue.pop() {
282            count += 1;
283            for child in self.children(node) {
284                queue.push(child);
285            }
286        }
287        count
288    }
289
290    // ── Bitvector primitives ────────────────────────────────────────
291
292    /// Count 1-bits in `bits[0..pos)`.
293    fn rank1(&self, pos: usize) -> usize {
294        if pos == 0 {
295            return 0;
296        }
297        let word_idx = pos / 64;
298        let bit_idx = pos % 64;
299
300        let sb_idx = word_idx / SUPERBLOCK_WORDS;
301        let mut count = self.rank_superblocks[sb_idx] as usize;
302
303        let sb_start = sb_idx * SUPERBLOCK_WORDS;
304        for i in sb_start..word_idx.min(self.bits.len()) {
305            count += self.bits[i].count_ones() as usize;
306        }
307
308        if bit_idx > 0 && word_idx < self.bits.len() {
309            let mask = (1u64 << bit_idx) - 1;
310            count += (self.bits[word_idx] & mask).count_ones() as usize;
311        }
312
313        count
314    }
315
316    /// Count 0-bits in `bits[0..pos)`.
317    fn rank0(&self, pos: usize) -> usize {
318        pos - self.rank1(pos)
319    }
320
321    /// Find position of the `k`-th 1-bit (0-indexed).
322    fn select1(&self, k: usize) -> usize {
323        let target = k as u64;
324        let mut lo = 0usize;
325        let mut hi = self.rank_superblocks.len() - 1;
326        while lo < hi {
327            let mid = lo + (hi - lo) / 2;
328            if self.rank_superblocks[mid + 1] <= target {
329                lo = mid + 1;
330            } else {
331                hi = mid;
332            }
333        }
334
335        let sb = lo;
336        let mut remaining = k - self.rank_superblocks[sb] as usize;
337        let word_start = sb * SUPERBLOCK_WORDS;
338
339        for w in word_start..self.bits.len() {
340            let ones = self.bits[w].count_ones() as usize;
341            if remaining < ones {
342                return w * 64 + select_in_word(self.bits[w], remaining);
343            }
344            remaining -= ones;
345        }
346
347        panic!("select1({k}): not enough 1-bits")
348    }
349
350    /// Find position of the `k`-th 0-bit (0-indexed).
351    fn select0(&self, k: usize) -> usize {
352        let mut lo = 0usize;
353        let mut hi = self.rank_superblocks.len() - 1;
354        while lo < hi {
355            let mid = lo + (hi - lo) / 2;
356            let total_bits = (mid + 1) * SUPERBLOCK_WORDS * 64;
357            let ones = self.rank_superblocks[mid + 1] as usize;
358            let zeros = total_bits - ones;
359            if zeros <= k {
360                lo = mid + 1;
361            } else {
362                hi = mid;
363            }
364        }
365
366        let sb = lo;
367        let sb_total_bits = sb * SUPERBLOCK_WORDS * 64;
368        let sb_ones = self.rank_superblocks[sb] as usize;
369        let mut remaining = k - (sb_total_bits - sb_ones);
370        let word_start = sb * SUPERBLOCK_WORDS;
371
372        for w in word_start..self.bits.len() {
373            let zeros = self.bits[w].count_zeros() as usize;
374            if remaining < zeros {
375                return w * 64 + select0_in_word(self.bits[w], remaining);
376            }
377            remaining -= zeros;
378        }
379
380        panic!("select0({k}): not enough 0-bits")
381    }
382}
383
384/// Iterator over children of a node.
385pub struct ChildIter<'a> {
386    tree: &'a LoudsTree,
387    next: Option<usize>,
388}
389
390impl Iterator for ChildIter<'_> {
391    type Item = usize;
392
393    fn next(&mut self) -> Option<usize> {
394        let v = self.next?;
395        self.next = self.tree.next_sibling(v);
396        Some(v)
397    }
398}
399
400// ── Bit helpers ─────────────────────────────────────────────────────
401
402/// Set bit at position `pos` in the bitvector.
403fn set_bit(bits: &mut [u64], pos: usize) {
404    let word = pos / 64;
405    let bit = pos % 64;
406    bits[word] |= 1u64 << bit;
407}
408
409/// Get bit at position `pos` in the bitvector.
410fn get_bit(bits: &[u64], pos: usize) -> bool {
411    let word = pos / 64;
412    let bit = pos % 64;
413    (bits[word] >> bit) & 1 == 1
414}
415
416/// Build rank superblocks for a bitvector.
417fn build_rank_superblocks(bits: &[u64]) -> Vec<u64> {
418    let num_superblocks = bits.len().div_ceil(SUPERBLOCK_WORDS);
419    let mut superblocks = Vec::with_capacity(num_superblocks + 1);
420    superblocks.push(0u64);
421    let mut cumulative = 0u64;
422    for chunk in bits.chunks(SUPERBLOCK_WORDS) {
423        for &word in chunk {
424            cumulative += word.count_ones() as u64;
425        }
426        superblocks.push(cumulative);
427    }
428    superblocks
429}
430
431/// Find position of the `k`-th 1-bit within a u64 word (0-indexed).
432fn select_in_word(word: u64, k: usize) -> usize {
433    let mut remaining = k;
434    let mut w = word;
435    for bit in 0..64 {
436        if w & 1 == 1 {
437            if remaining == 0 {
438                return bit;
439            }
440            remaining -= 1;
441        }
442        w >>= 1;
443        if w == 0 {
444            break;
445        }
446    }
447    unreachable!("select_in_word: not enough 1-bits")
448}
449
450/// Find position of the `k`-th 0-bit within a u64 word (0-indexed).
451fn select0_in_word(word: u64, k: usize) -> usize {
452    let mut remaining = k;
453    let inverted = !word;
454    let mut w = inverted;
455    for bit in 0..64 {
456        if w & 1 == 1 {
457            if remaining == 0 {
458                return bit;
459            }
460            remaining -= 1;
461        }
462        w >>= 1;
463    }
464    unreachable!("select0_in_word: not enough 0-bits")
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    // ── Construction ───────────────────────────────────────────────
472
473    #[test]
474    fn single_node_tree() {
475        let tree = LoudsTree::from_degrees(&[0]);
476        assert_eq!(tree.node_count(), 1);
477        assert!(tree.is_leaf(0));
478        assert_eq!(tree.parent(0), None);
479        assert_eq!(tree.first_child(0), None);
480        assert_eq!(tree.depth(0), 0);
481    }
482
483    #[test]
484    fn linear_chain() {
485        // root -> a -> b -> c
486        let tree = LoudsTree::from_degrees(&[1, 1, 1, 0]);
487        assert_eq!(tree.node_count(), 4);
488
489        assert_eq!(tree.parent(0), None);
490        assert_eq!(tree.parent(1), Some(0));
491        assert_eq!(tree.parent(2), Some(1));
492        assert_eq!(tree.parent(3), Some(2));
493
494        assert_eq!(tree.first_child(0), Some(1));
495        assert_eq!(tree.first_child(1), Some(2));
496        assert_eq!(tree.first_child(2), Some(3));
497        assert!(tree.is_leaf(3));
498
499        assert_eq!(tree.depth(0), 0);
500        assert_eq!(tree.depth(1), 1);
501        assert_eq!(tree.depth(2), 2);
502        assert_eq!(tree.depth(3), 3);
503    }
504
505    #[test]
506    fn binary_tree() {
507        //       0
508        //      / \
509        //     1   2
510        //    / \
511        //   3   4
512        let tree = LoudsTree::from_degrees(&[2, 2, 0, 0, 0]);
513        assert_eq!(tree.node_count(), 5);
514
515        assert_eq!(tree.first_child(0), Some(1));
516        assert_eq!(tree.next_sibling(1), Some(2));
517        assert_eq!(tree.next_sibling(2), None);
518
519        assert_eq!(tree.first_child(1), Some(3));
520        assert_eq!(tree.next_sibling(3), Some(4));
521        assert!(tree.is_leaf(3));
522        assert!(tree.is_leaf(4));
523        assert!(tree.is_leaf(2));
524
525        assert_eq!(tree.parent(1), Some(0));
526        assert_eq!(tree.parent(2), Some(0));
527        assert_eq!(tree.parent(3), Some(1));
528        assert_eq!(tree.parent(4), Some(1));
529    }
530
531    #[test]
532    fn wide_tree() {
533        //       0
534        //    / | | \
535        //   1  2  3  4
536        let tree = LoudsTree::from_degrees(&[4, 0, 0, 0, 0]);
537        assert_eq!(tree.node_count(), 5);
538
539        assert_eq!(tree.first_child(0), Some(1));
540        assert_eq!(tree.next_sibling(1), Some(2));
541        assert_eq!(tree.next_sibling(2), Some(3));
542        assert_eq!(tree.next_sibling(3), Some(4));
543        assert_eq!(tree.next_sibling(4), None);
544
545        for i in 1..5 {
546            assert!(tree.is_leaf(i));
547            assert_eq!(tree.parent(i), Some(0));
548            assert_eq!(tree.depth(i), 1);
549        }
550    }
551
552    #[test]
553    fn three_level_tree() {
554        //       0
555        //      / \
556        //     1   2
557        //    |   / \
558        //    3  4   5
559        let tree = LoudsTree::from_degrees(&[2, 1, 2, 0, 0, 0]);
560        assert_eq!(tree.node_count(), 6);
561
562        assert_eq!(tree.first_child(0), Some(1));
563        assert_eq!(tree.next_sibling(1), Some(2));
564        assert_eq!(tree.first_child(1), Some(3));
565        assert_eq!(tree.first_child(2), Some(4));
566        assert_eq!(tree.next_sibling(4), Some(5));
567
568        assert_eq!(tree.parent(3), Some(1));
569        assert_eq!(tree.parent(4), Some(2));
570        assert_eq!(tree.parent(5), Some(2));
571
572        assert_eq!(tree.depth(4), 2);
573        assert_eq!(tree.depth(5), 2);
574    }
575
576    // ── Degree ────────────────────────────────────────────────────
577
578    #[test]
579    fn degree_matches_input() {
580        let degrees = [2, 1, 2, 0, 0, 0];
581        let tree = LoudsTree::from_degrees(&degrees);
582        for (i, &d) in degrees.iter().enumerate() {
583            assert_eq!(tree.degree(i), d, "degree mismatch at node {i}");
584        }
585    }
586
587    // ── Children iterator ─────────────────────────────────────────
588
589    #[test]
590    fn children_iter() {
591        let tree = LoudsTree::from_degrees(&[3, 0, 1, 0, 0]);
592        let root_children: Vec<_> = tree.children(0).collect();
593        assert_eq!(root_children, vec![1, 2, 3]);
594
595        let node2_children: Vec<_> = tree.children(2).collect();
596        assert_eq!(node2_children, vec![4]);
597
598        let leaf_children: Vec<_> = tree.children(1).collect();
599        assert!(leaf_children.is_empty());
600    }
601
602    // ── Subtree size ──────────────────────────────────────────────
603
604    #[test]
605    fn subtree_size_root() {
606        let tree = LoudsTree::from_degrees(&[2, 2, 0, 0, 0]);
607        assert_eq!(tree.subtree_size(0), 5);
608    }
609
610    #[test]
611    fn subtree_size_leaf() {
612        let tree = LoudsTree::from_degrees(&[2, 2, 0, 0, 0]);
613        assert_eq!(tree.subtree_size(3), 1);
614    }
615
616    #[test]
617    fn subtree_size_internal() {
618        let tree = LoudsTree::from_degrees(&[2, 2, 0, 0, 0]);
619        // Node 1 has children 3, 4
620        assert_eq!(tree.subtree_size(1), 3);
621    }
622
623    // ── from_children ─────────────────────────────────────────────
624
625    #[test]
626    fn from_children_matches_degrees() {
627        let children: &[&[usize]] = &[&[1, 2], &[3, 4], &[], &[], &[]];
628        let tree = LoudsTree::from_children(children);
629        assert_eq!(tree.node_count(), 5);
630        assert_eq!(tree.first_child(0), Some(1));
631        assert_eq!(tree.first_child(1), Some(3));
632    }
633
634    // ── Memory efficiency ─────────────────────────────────────────
635
636    #[test]
637    fn memory_much_less_than_pointers() {
638        let n = 1000;
639        // Complete binary tree with n leaves (2n-1 nodes total)
640        // Build degree sequence in BFS order
641        let total = 2 * n - 1;
642        let mut degrees = vec![0usize; total];
643        for d in degrees.iter_mut().take(n - 1) {
644            *d = 2;
645        }
646        let tree = LoudsTree::from_degrees(&degrees);
647
648        let louds_bytes = tree.size_in_bytes();
649        let pointer_bytes = total * 3 * 8; // parent + first_child + next_sibling pointers
650        assert!(
651            louds_bytes < pointer_bytes / 10,
652            "LOUDS ({louds_bytes}B) should be < 10% of pointer tree ({pointer_bytes}B)"
653        );
654    }
655
656    #[test]
657    fn memory_scaling() {
658        for &n in &[100, 1000, 10_000] {
659            let total = 2 * n - 1;
660            let mut degrees = vec![0usize; total];
661            for d in degrees.iter_mut().take(n - 1) {
662                *d = 2;
663            }
664            let tree = LoudsTree::from_degrees(&degrees);
665            let bits_per_node = (tree.size_in_bytes() * 8) as f64 / total as f64;
666            // LOUDS uses ~2 bits per node plus superblock overhead
667            assert!(
668                bits_per_node < 4.0,
669                "n={n}: {bits_per_node:.1} bits/node exceeds 4.0"
670            );
671        }
672    }
673
674    // ── Edge cases ────────────────────────────────────────────────
675
676    #[test]
677    fn root_no_siblings() {
678        let tree = LoudsTree::from_degrees(&[2, 0, 0]);
679        assert_eq!(tree.next_sibling(0), None);
680    }
681
682    #[test]
683    #[should_panic(expected = "degree sum")]
684    fn invalid_degree_sum_panics() {
685        LoudsTree::from_degrees(&[3, 0, 0]); // sum=3, n-1=2
686    }
687
688    #[test]
689    #[should_panic(expected = "must not be empty")]
690    fn empty_degrees_panics() {
691        LoudsTree::from_degrees(&[]);
692    }
693
694    // ── Property: parent-child consistency ────────────────────────
695
696    #[test]
697    fn parent_child_roundtrip() {
698        // For every non-root node, parent(first_child(parent(v))) or a sibling == v
699        let tree = LoudsTree::from_degrees(&[3, 2, 0, 1, 0, 0, 0]);
700        for v in 1..tree.node_count() {
701            let p = tree.parent(v).unwrap();
702            // v should be reachable from p's children
703            let children: Vec<_> = tree.children(p).collect();
704            assert!(
705                children.contains(&v),
706                "node {v}'s parent is {p} but {v} not in parent's children: {children:?}"
707            );
708        }
709    }
710
711    #[test]
712    fn all_nodes_reachable_from_root() {
713        let tree = LoudsTree::from_degrees(&[3, 2, 0, 1, 0, 0, 0]);
714        let mut visited = vec![false; tree.node_count()];
715        let mut stack = vec![0usize];
716        while let Some(v) = stack.pop() {
717            visited[v] = true;
718            for child in tree.children(v) {
719                stack.push(child);
720            }
721        }
722        assert!(visited.iter().all(|&v| v), "not all nodes reachable");
723    }
724
725    // ── Proptest ──────────────────────────────────────────────────
726
727    #[cfg(test)]
728    mod proptests {
729        use super::*;
730        use proptest::prelude::*;
731
732        /// Generate a valid BFS degree sequence for a tree with n nodes.
733        ///
734        /// In a valid BFS sequence, after processing node i, the cumulative
735        /// child count must be at least i+1 (so that node i+1 exists).
736        fn arb_degree_sequence(max_nodes: usize) -> impl Strategy<Value = Vec<usize>> {
737            (2..=max_nodes).prop_flat_map(|n| {
738                prop::collection::vec(0..=4usize, n).prop_map(move |raw| {
739                    let mut degrees = vec![0usize; n];
740                    let mut total_children: usize = 0;
741                    let target = n - 1;
742
743                    for i in 0..n {
744                        let remaining = target - total_children;
745                        if remaining == 0 {
746                            break;
747                        }
748                        // Must generate enough children so that node i+1 exists.
749                        // After assigning degree to node i, cumulative children
750                        // must be >= i + 1.
751                        let min_needed = (i + 1).saturating_sub(total_children);
752                        let max_allowed = remaining.min(raw[i].max(min_needed));
753                        let d = max_allowed.max(min_needed);
754                        degrees[i] = d;
755                        total_children += d;
756                    }
757
758                    // If we still haven't assigned enough children, add to the
759                    // first node that can absorb them.
760                    let deficit = target.saturating_sub(total_children);
761                    if deficit > 0 {
762                        degrees[0] += deficit;
763                    }
764
765                    degrees
766                })
767            })
768        }
769
770        proptest! {
771            #[test]
772            fn navigation_consistent(degrees in arb_degree_sequence(50)) {
773                let tree = LoudsTree::from_degrees(&degrees);
774                let n = tree.node_count();
775                prop_assert_eq!(n, degrees.len());
776
777                // Every non-root has a parent
778                for v in 1..n {
779                    let p = tree.parent(v);
780                    prop_assert!(p.is_some(), "node {v} has no parent");
781                    prop_assert!(p.unwrap() < v, "parent {} >= child {v}", p.unwrap());
782                }
783
784                // Parent-child consistency
785                for v in 0..n {
786                    for child in tree.children(v) {
787                        prop_assert_eq!(tree.parent(child), Some(v));
788                    }
789                }
790
791                // Degree matches children count
792                for (v, &expected_deg) in degrees.iter().enumerate().take(n) {
793                    let child_count = tree.children(v).count();
794                    prop_assert_eq!(tree.degree(v), child_count);
795                    prop_assert_eq!(tree.degree(v), expected_deg);
796                }
797            }
798
799            #[test]
800            fn subtree_sizes_sum(degrees in arb_degree_sequence(30)) {
801                let tree = LoudsTree::from_degrees(&degrees);
802                // Root subtree = entire tree
803                prop_assert_eq!(tree.subtree_size(0), tree.node_count());
804            }
805
806            #[test]
807            fn depth_matches_parent_chain(degrees in arb_degree_sequence(50)) {
808                let tree = LoudsTree::from_degrees(&degrees);
809                for v in 0..tree.node_count() {
810                    let d = tree.depth(v);
811                    if v == 0 {
812                        prop_assert_eq!(d, 0);
813                    } else {
814                        let parent_depth = tree.depth(tree.parent(v).unwrap());
815                        prop_assert_eq!(d, parent_depth + 1);
816                    }
817                }
818            }
819
820            #[test]
821            fn memory_sublinear(n in 50..500usize) {
822                let total = 2 * n - 1;
823                let mut degrees = vec![0usize; total];
824                for d in degrees.iter_mut().take(n - 1) {
825                    *d = 2;
826                }
827                let tree = LoudsTree::from_degrees(&degrees);
828                let bits_per_node = (tree.size_in_bytes() * 8) as f64 / total as f64;
829                // ~2 bits/node + superblock overhead; 5 bits is generous
830                prop_assert!(bits_per_node < 5.0,
831                    "n={n}: {bits_per_node:.1} bits/node exceeds 5.0");
832            }
833        }
834    }
835}