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}