Skip to main content

rust_igraph/algorithms/properties/
is_caterpillar.rs

1//! Caterpillar tree predicate (ALGO-PR-086).
2//!
3//! A caterpillar tree is a tree in which every vertex is within
4//! distance 1 of a central path (the "spine"). Equivalently, removing
5//! all leaf vertices from a caterpillar yields a path (or an empty
6//! graph or a single vertex).
7//!
8//! For directed graphs, the function returns `false`.
9
10use crate::algorithms::paths::dijkstra::DijkstraMode;
11use crate::algorithms::properties::is_tree::is_tree;
12use crate::core::{Graph, IgraphResult};
13
14/// Check whether a graph is a caterpillar tree.
15///
16/// Returns `false` for directed graphs and non-tree graphs.
17/// Returns `true` for the empty graph, single vertex, single edge,
18/// paths, and stars (all are caterpillars).
19///
20/// # Examples
21///
22/// ```
23/// use rust_igraph::{Graph, is_caterpillar};
24///
25/// // Star `K_{1,4}` is a caterpillar (spine is just the center)
26/// let mut g = Graph::with_vertices(5);
27/// for i in 1..5u32 {
28///     g.add_edge(0, i).unwrap();
29/// }
30/// assert!(is_caterpillar(&g).unwrap());
31///
32/// // Spider (3 legs of length 2 from a center) is NOT a caterpillar
33/// // Center 0 → 1→4, 2→5, 3→6
34/// let mut g = Graph::with_vertices(7);
35/// g.add_edge(0, 1).unwrap();
36/// g.add_edge(0, 2).unwrap();
37/// g.add_edge(0, 3).unwrap();
38/// g.add_edge(1, 4).unwrap();
39/// g.add_edge(2, 5).unwrap();
40/// g.add_edge(3, 6).unwrap();
41/// assert!(!is_caterpillar(&g).unwrap());
42/// ```
43pub fn is_caterpillar(graph: &Graph) -> IgraphResult<bool> {
44    if graph.is_directed() {
45        return Ok(false);
46    }
47
48    let n = graph.vcount();
49    if n == 0 {
50        return Ok(true);
51    }
52    if n <= 2 {
53        return Ok(is_tree(graph, DijkstraMode::All)?.is_some());
54    }
55
56    if is_tree(graph, DijkstraMode::All)?.is_none() {
57        return Ok(false);
58    }
59
60    // Compute degrees
61    let mut deg = vec![0u32; n as usize];
62    for v in 0..n {
63        deg[v as usize] = u32::try_from(graph.degree(v)?).unwrap_or(u32::MAX);
64    }
65
66    // A vertex is a leaf if degree == 1.
67    // The "spine" consists of all non-leaf vertices.
68    // For a caterpillar, the spine must form a path:
69    //   - every spine vertex has at most 2 spine neighbors
70    //   - the spine is connected (guaranteed since the tree is connected
71    //     and removing leaves from a tree preserves connectivity of the
72    //     remaining vertices, as long as ≥ 1 non-leaf exists)
73    let is_leaf = |v: usize| deg[v] == 1;
74
75    let spine_count = (0..n as usize).filter(|&v| !is_leaf(v)).count();
76    if spine_count <= 1 {
77        return Ok(true);
78    }
79
80    // For each spine vertex, count its spine neighbors.
81    // In a path, every vertex has at most 2 neighbors.
82    for v in 0..n {
83        if is_leaf(v as usize) {
84            continue;
85        }
86        let nbrs = graph.neighbors(v)?;
87        let spine_nbr_count = nbrs.iter().filter(|&&w| !is_leaf(w as usize)).count();
88        if spine_nbr_count > 2 {
89            return Ok(false);
90        }
91    }
92
93    Ok(true)
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn empty_graph() {
102        let g = Graph::with_vertices(0);
103        assert!(is_caterpillar(&g).unwrap());
104    }
105
106    #[test]
107    fn single_vertex() {
108        let g = Graph::with_vertices(1);
109        assert!(is_caterpillar(&g).unwrap());
110    }
111
112    #[test]
113    fn single_edge() {
114        let mut g = Graph::with_vertices(2);
115        g.add_edge(0, 1).unwrap();
116        assert!(is_caterpillar(&g).unwrap());
117    }
118
119    #[test]
120    fn path_p3() {
121        let mut g = Graph::with_vertices(3);
122        g.add_edge(0, 1).unwrap();
123        g.add_edge(1, 2).unwrap();
124        assert!(is_caterpillar(&g).unwrap());
125    }
126
127    #[test]
128    fn path_p5() {
129        let mut g = Graph::with_vertices(5);
130        g.add_edge(0, 1).unwrap();
131        g.add_edge(1, 2).unwrap();
132        g.add_edge(2, 3).unwrap();
133        g.add_edge(3, 4).unwrap();
134        assert!(is_caterpillar(&g).unwrap());
135    }
136
137    #[test]
138    fn star_k14() {
139        let mut g = Graph::with_vertices(5);
140        for i in 1..5u32 {
141            g.add_edge(0, i).unwrap();
142        }
143        assert!(is_caterpillar(&g).unwrap());
144    }
145
146    #[test]
147    fn caterpillar_with_spine() {
148        // Spine: 0-1-2-3, leaves: 4,5 on vertex 1; 6 on vertex 2
149        let mut g = Graph::with_vertices(7);
150        g.add_edge(0, 1).unwrap();
151        g.add_edge(1, 2).unwrap();
152        g.add_edge(2, 3).unwrap();
153        g.add_edge(1, 4).unwrap();
154        g.add_edge(1, 5).unwrap();
155        g.add_edge(2, 6).unwrap();
156        assert!(is_caterpillar(&g).unwrap());
157    }
158
159    #[test]
160    fn spider_not_caterpillar() {
161        // Center 0, three legs of length 2: 0-1-4, 0-2-5, 0-3-6
162        // After removing leaves (4,5,6), spine is 0-1, 0-2, 0-3 (star)
163        // Vertex 0 has 3 spine neighbors → not a path → not caterpillar
164        let mut g = Graph::with_vertices(7);
165        g.add_edge(0, 1).unwrap();
166        g.add_edge(0, 2).unwrap();
167        g.add_edge(0, 3).unwrap();
168        g.add_edge(1, 4).unwrap();
169        g.add_edge(2, 5).unwrap();
170        g.add_edge(3, 6).unwrap();
171        assert!(!is_caterpillar(&g).unwrap());
172    }
173
174    #[test]
175    fn t_shape_not_caterpillar() {
176        // T-shape: spine has a branch
177        // 0-1-2-3, with 4-1 AND 4-5 (leg of length 2 from 1)
178        // Wait, let me be more precise.
179        // Tree: 0-1, 1-2, 2-3, 1-4, 4-5
180        // Leaves: 0, 3, 5. Spine: 1, 2, 4
181        // Spine neighbors: 1 has spine neighbors {2, 4}, 2 has {1}, 4 has {1}
182        // Max spine degree = 2 → this IS a caterpillar
183        // Let me make a proper non-caterpillar:
184        // Tree: 0-1, 1-2, 2-3, 3-4, 1-5, 5-6, 5-7
185        // Leaves: 0, 4, 6, 7. Spine: 1, 2, 3, 5
186        // Spine neighbors of 1: {2, 5} → 2
187        // Spine neighbors of 2: {1, 3} → 2
188        // Spine neighbors of 3: {2} → 1
189        // Spine neighbors of 5: {1} → 1
190        // All ≤ 2 → still a caterpillar!
191        // For non-caterpillar, need 3+ branches of length ≥ 2
192        // 0-C, C-1, C-2, C-3, 1-4, 2-5, 3-6 (where C = center)
193        // Leaves: 4, 5, 6 (and 0 is a leaf too)
194        // Non-leaves: C, 1, 2, 3
195        // C has spine neighbors {1, 2, 3} → 3 → not caterpillar
196        let mut g = Graph::with_vertices(8);
197        g.add_edge(0, 7).unwrap(); // 0 is leaf of C=7
198        g.add_edge(7, 1).unwrap();
199        g.add_edge(7, 2).unwrap();
200        g.add_edge(7, 3).unwrap();
201        g.add_edge(1, 4).unwrap();
202        g.add_edge(2, 5).unwrap();
203        g.add_edge(3, 6).unwrap();
204        // Leaves: 0, 4, 5, 6. Spine: 7, 1, 2, 3.
205        // 7 has spine neighbors {1, 2, 3} → 3 → not caterpillar
206        assert!(!is_caterpillar(&g).unwrap());
207    }
208
209    #[test]
210    fn triangle_not_caterpillar() {
211        // Not a tree → not a caterpillar
212        let mut g = Graph::with_vertices(3);
213        g.add_edge(0, 1).unwrap();
214        g.add_edge(1, 2).unwrap();
215        g.add_edge(2, 0).unwrap();
216        assert!(!is_caterpillar(&g).unwrap());
217    }
218
219    #[test]
220    fn directed_returns_false() {
221        let mut g = Graph::new(3, true).unwrap();
222        g.add_edge(0, 1).unwrap();
223        g.add_edge(1, 2).unwrap();
224        assert!(!is_caterpillar(&g).unwrap());
225    }
226
227    #[test]
228    fn disconnected_not_caterpillar() {
229        // Two components → not a tree → not a caterpillar
230        let mut g = Graph::with_vertices(4);
231        g.add_edge(0, 1).unwrap();
232        g.add_edge(2, 3).unwrap();
233        assert!(!is_caterpillar(&g).unwrap());
234    }
235
236    #[test]
237    fn double_star() {
238        // Two stars joined by an edge: center1-center2, with leaves
239        // Spine: center1-center2 (path of length 1)
240        let mut g = Graph::with_vertices(8);
241        g.add_edge(0, 1).unwrap(); // spine edge
242        g.add_edge(0, 2).unwrap();
243        g.add_edge(0, 3).unwrap();
244        g.add_edge(0, 4).unwrap();
245        g.add_edge(1, 5).unwrap();
246        g.add_edge(1, 6).unwrap();
247        g.add_edge(1, 7).unwrap();
248        assert!(is_caterpillar(&g).unwrap());
249    }
250
251    #[test]
252    fn binary_tree_not_caterpillar() {
253        // Complete binary tree of depth 2: root 0, children 1,2,
254        // grandchildren 3,4,5,6
255        let mut g = Graph::with_vertices(7);
256        g.add_edge(0, 1).unwrap();
257        g.add_edge(0, 2).unwrap();
258        g.add_edge(1, 3).unwrap();
259        g.add_edge(1, 4).unwrap();
260        g.add_edge(2, 5).unwrap();
261        g.add_edge(2, 6).unwrap();
262        // Leaves: 3,4,5,6. Spine: 0,1,2.
263        // 0 has spine neighbors {1,2} → 2
264        // 1 has spine neighbor {0} → 1
265        // 2 has spine neighbor {0} → 1
266        // All ≤ 2 → caterpillar!
267        // Actually yes, a complete binary tree of depth 2 IS a caterpillar
268        // (spine is 1-0-2, a path)
269        assert!(is_caterpillar(&g).unwrap());
270    }
271
272    #[test]
273    fn lobster_not_caterpillar() {
274        // Lobster: needs distance 2 from spine
275        // Spine: 0-1, with 1-2, 2-3, 2-4 (branch of depth 2)
276        // And 0-5, 5-6 (another branch of depth 2)
277        // Leaves: 3, 4, 6. Also 1 connects to ... wait.
278        // Let me be precise.
279        // 0-1, 0-5, 1-2, 2-3, 2-4, 5-6
280        // Degrees: 0→2, 1→2, 2→3, 3→1, 4→1, 5→2, 6→1
281        // Leaves: 3, 4, 6. Non-leaves: 0, 1, 2, 5
282        // Spine neighbors: 0→{1,5}=2, 1→{0,2}=2, 2→{1}=1, 5→{0}=1
283        // All ≤ 2 → still a caterpillar!
284        // Need a more extreme example. Let me construct a true lobster.
285        // Spine: A-B, branch A→C→D (depth 2 off A), branch B→E→F (depth 2 off B)
286        // Edges: A-B, A-C, C-D, B-E, E-F
287        // Leaves: D, F. Non-leaves: A, B, C, E
288        // A spine neighbors: {B, C} → 2
289        // B spine neighbors: {A, E} → 2
290        // C spine neighbors: {A} → 1
291        // E spine neighbors: {B} → 1
292        // All ≤ 2 → caterpillar!
293        // Hmm, any tree where removing leaves gives a path is a caterpillar.
294        // A lobster adds one more level of leaves.
295        // The lobster itself is a caterpillar if its spine (after removing leaves) is a path.
296        // But wait, removing leaves from a lobster gives a caterpillar, not just a path.
297        // So a lobster is NOT necessarily a caterpillar.
298        // Example: take a path 0-1-2, attach 3,4 to 1. Now from 3 attach 5.
299        // Edges: 0-1, 1-2, 1-3, 1-4, 3-5
300        // Degrees: 0→1, 1→4, 2→1, 3→2, 4→1, 5→1
301        // Leaves: 0, 2, 4, 5. Spine: 1, 3
302        // Spine neighbors of 1: {3} → 1
303        // Spine neighbors of 3: {1} → 1
304        // Spine is a path → caterpillar!
305        // Argh. I need branches from DIFFERENT spine vertices that are themselves long.
306        // The key insight: for a tree to NOT be a caterpillar, the spine must not be a path.
307        // The spine is not a path when some spine vertex has 3+ spine neighbors.
308        // This happens when 3+ branches of depth ≥ 2 emanate from the same vertex.
309        // Already tested as spider_not_caterpillar above.
310        // Let me add a test for "lobster that IS a caterpillar":
311        let mut g = Graph::with_vertices(8);
312        g.add_edge(0, 1).unwrap(); // spine
313        g.add_edge(1, 2).unwrap(); // spine
314        g.add_edge(0, 3).unwrap(); // leaf
315        g.add_edge(1, 4).unwrap(); // branch depth 1
316        g.add_edge(4, 5).unwrap(); // branch depth 2 (leaf)
317        g.add_edge(2, 6).unwrap(); // branch depth 1
318        g.add_edge(6, 7).unwrap(); // branch depth 2 (leaf)
319        // Leaves: 3, 5, 7. Non-leaves: 0, 1, 2, 4, 6
320        // Spine nbrs: 0→{1}=1, 1→{0,2,4}=3 → NOT caterpillar!
321        assert!(!is_caterpillar(&g).unwrap());
322    }
323}