rust_igraph/algorithms/properties/is_lobster.rs
1//! Lobster tree predicate (ALGO-PR-087).
2//!
3//! A lobster is a tree in which every vertex is within distance 2 of
4//! a central path. Equivalently, removing all leaf vertices from a
5//! lobster yields a caterpillar (and removing leaves again yields a
6//! path).
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 lobster tree.
15///
16/// A lobster is a tree where every vertex is within distance 2 of a
17/// central path. Equivalently, removing all leaves twice yields a path
18/// (or empty/single vertex).
19///
20/// Returns `false` for directed graphs and non-tree graphs.
21/// Returns `true` for the empty graph, single vertex, paths, stars,
22/// and caterpillars (all are lobsters).
23///
24/// # Examples
25///
26/// ```
27/// use rust_igraph::{Graph, is_lobster};
28///
29/// // Star is a lobster (it's even a caterpillar)
30/// let mut g = Graph::with_vertices(5);
31/// for i in 1..5u32 {
32/// g.add_edge(0, i).unwrap();
33/// }
34/// assert!(is_lobster(&g).unwrap());
35///
36/// // Caterpillar with secondary branches → lobster
37/// let mut g = Graph::with_vertices(8);
38/// g.add_edge(0, 1).unwrap(); // spine
39/// g.add_edge(1, 2).unwrap(); // spine
40/// g.add_edge(0, 3).unwrap(); // leaf off spine
41/// g.add_edge(1, 4).unwrap(); // branch
42/// g.add_edge(4, 5).unwrap(); // leaf off branch (distance 2 from spine)
43/// g.add_edge(2, 6).unwrap(); // branch
44/// g.add_edge(6, 7).unwrap(); // leaf off branch
45/// assert!(is_lobster(&g).unwrap());
46/// ```
47pub fn is_lobster(graph: &Graph) -> IgraphResult<bool> {
48 if graph.is_directed() {
49 return Ok(false);
50 }
51
52 let n = graph.vcount();
53 if n == 0 {
54 return Ok(true);
55 }
56
57 if is_tree(graph, DijkstraMode::All)?.is_none() {
58 return Ok(false);
59 }
60
61 // Compute degrees
62 let mut deg = vec![0u32; n as usize];
63 for v in 0..n {
64 deg[v as usize] = u32::try_from(graph.degree(v)?).unwrap_or(u32::MAX);
65 }
66
67 // First pass: identify leaves (degree 1) and compute "pruned degrees"
68 // (degree after removing leaves).
69 let is_leaf1 = |v: usize| deg[v] == 1;
70
71 let mut pruned_deg = vec![0u32; n as usize];
72 for v in 0..n {
73 let vi = v as usize;
74 if is_leaf1(vi) {
75 continue;
76 }
77 let nbrs = graph.neighbors(v)?;
78 pruned_deg[vi] = u32::try_from(nbrs.iter().filter(|&&w| !is_leaf1(w as usize)).count())
79 .unwrap_or(u32::MAX);
80 }
81
82 // Second pass: in the pruned graph, identify new leaves (pruned_deg == 1)
83 // and check that the doubly-pruned graph is a path (all remaining
84 // vertices have at most 2 non-leaf-1, non-leaf-2 neighbors).
85 let is_leaf2 = |v: usize| !is_leaf1(v) && pruned_deg[v] <= 1;
86
87 for v in 0..n {
88 let vi = v as usize;
89 if is_leaf1(vi) || is_leaf2(vi) {
90 continue;
91 }
92 let nbrs = graph.neighbors(v)?;
93 let spine_nbrs = nbrs
94 .iter()
95 .filter(|&&w| !is_leaf1(w as usize) && !is_leaf2(w as usize))
96 .count();
97 if spine_nbrs > 2 {
98 return Ok(false);
99 }
100 }
101
102 Ok(true)
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn empty_graph() {
111 let g = Graph::with_vertices(0);
112 assert!(is_lobster(&g).unwrap());
113 }
114
115 #[test]
116 fn single_vertex() {
117 let g = Graph::with_vertices(1);
118 assert!(is_lobster(&g).unwrap());
119 }
120
121 #[test]
122 fn single_edge() {
123 let mut g = Graph::with_vertices(2);
124 g.add_edge(0, 1).unwrap();
125 assert!(is_lobster(&g).unwrap());
126 }
127
128 #[test]
129 fn path() {
130 let mut g = Graph::with_vertices(5);
131 g.add_edge(0, 1).unwrap();
132 g.add_edge(1, 2).unwrap();
133 g.add_edge(2, 3).unwrap();
134 g.add_edge(3, 4).unwrap();
135 assert!(is_lobster(&g).unwrap());
136 }
137
138 #[test]
139 fn star() {
140 let mut g = Graph::with_vertices(5);
141 for i in 1..5u32 {
142 g.add_edge(0, i).unwrap();
143 }
144 assert!(is_lobster(&g).unwrap());
145 }
146
147 #[test]
148 fn caterpillar_is_lobster() {
149 // Spine: 0-1-2-3, leaves: 4,5 on 1; 6 on 2
150 let mut g = Graph::with_vertices(7);
151 g.add_edge(0, 1).unwrap();
152 g.add_edge(1, 2).unwrap();
153 g.add_edge(2, 3).unwrap();
154 g.add_edge(1, 4).unwrap();
155 g.add_edge(1, 5).unwrap();
156 g.add_edge(2, 6).unwrap();
157 assert!(is_lobster(&g).unwrap());
158 }
159
160 #[test]
161 fn lobster_with_depth_2_branches() {
162 // Spine: 0-1, branches: 1→2→3, 1→4→5, 0→6→7
163 let mut g = Graph::with_vertices(8);
164 g.add_edge(0, 1).unwrap();
165 g.add_edge(1, 2).unwrap();
166 g.add_edge(2, 3).unwrap();
167 g.add_edge(1, 4).unwrap();
168 g.add_edge(4, 5).unwrap();
169 g.add_edge(0, 6).unwrap();
170 g.add_edge(6, 7).unwrap();
171 assert!(is_lobster(&g).unwrap());
172 }
173
174 #[test]
175 fn depth_3_branch_not_lobster() {
176 // Spine: 0-1, branch of depth 3: 0→2→3→4→5
177 // After removing leaves (5): spine is 0-1, branch 0→2→3→4
178 // After removing leaves again (1,4): 0→2→3
179 // That's a path of 3 with 0 having spine-nbr 2. OK.
180 // Actually let me check: tree has edges 0-1, 0-2, 2-3, 3-4, 4-5
181 // Degrees: 0→2, 1→1, 2→2, 3→2, 4→2, 5→1
182 // Leaf1: {1, 5}. Pruned degrees: 0→1(only 2), 2→2(0,3), 3→2(2,4), 4→1(only 3)
183 // Leaf2: {0, 4} (pruned_deg ≤ 1). Doubly-pruned: {2, 3}
184 // 2 has non-leaf1-non-leaf2 neighbors: check nbrs of 2: [0,3]. 0 is leaf2, 3 is not → 1
185 // 3 has nbrs [2,4]. 4 is leaf2, 2 is not → 1
186 // All ≤ 2 → lobster! Hmm, this IS a lobster.
187 // For non-lobster, need depth ≥ 3 branches from a branching point.
188 // A tree is NOT a lobster iff after removing leaves twice, the
189 // remainder has a vertex of degree ≥ 3.
190 // Example: center C with 3 branches of depth 3:
191 // C→A→A2→A3, C→B→B2→B3, C→C'→C2→C3
192 let mut g = Graph::with_vertices(10);
193 g.add_edge(0, 1).unwrap(); // C-A
194 g.add_edge(1, 2).unwrap(); // A-A2
195 g.add_edge(2, 3).unwrap(); // A2-A3
196 g.add_edge(0, 4).unwrap(); // C-B
197 g.add_edge(4, 5).unwrap(); // B-B2
198 g.add_edge(5, 6).unwrap(); // B2-B3
199 g.add_edge(0, 7).unwrap(); // C-C'
200 g.add_edge(7, 8).unwrap(); // C'-C2
201 g.add_edge(8, 9).unwrap(); // C2-C3
202 // Degrees: 0→3, 1→2, 2→2, 3→1, 4→2, 5→2, 6→1, 7→2, 8→2, 9→1
203 // Leaf1: {3,6,9}. Pruned: 0→3(1,4,7), 1→2(0,2), 2→1(1), 4→2(0,5), 5→1(4), 7→2(0,8), 8→1(7)
204 // Leaf2: {2,5,8} (pruned_deg==1 and not leaf1)
205 // Doubly-pruned: {0,1,4,7}
206 // 0: nbrs not leaf1/leaf2: {1,4,7} → 3 → NOT lobster!
207 assert!(!is_lobster(&g).unwrap());
208 }
209
210 #[test]
211 fn triangle_not_lobster() {
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_lobster(&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_lobster(&g).unwrap());
225 }
226
227 #[test]
228 fn spider_is_lobster() {
229 // Spider with 3 legs of length 2: center→a→a2, center→b→b2, center→c→c2
230 // This is NOT a caterpillar but IS a lobster
231 let mut g = Graph::with_vertices(7);
232 g.add_edge(0, 1).unwrap();
233 g.add_edge(0, 2).unwrap();
234 g.add_edge(0, 3).unwrap();
235 g.add_edge(1, 4).unwrap();
236 g.add_edge(2, 5).unwrap();
237 g.add_edge(3, 6).unwrap();
238 // Leaf1: {4,5,6}. Pruned: 0→3(1,2,3), 1→1(0), 2→1(0), 3→1(0)
239 // Leaf2: {1,2,3}. Doubly-pruned: {0}
240 // Single vertex → lobster!
241 assert!(is_lobster(&g).unwrap());
242 }
243
244 #[test]
245 fn binary_tree_depth3_is_lobster() {
246 // Complete binary tree of depth 3: spine is 1-0-2 (path),
247 // all vertices within distance 2 of spine → lobster.
248 let mut g = Graph::with_vertices(15);
249 g.add_edge(0, 1).unwrap();
250 g.add_edge(0, 2).unwrap();
251 g.add_edge(1, 3).unwrap();
252 g.add_edge(1, 4).unwrap();
253 g.add_edge(2, 5).unwrap();
254 g.add_edge(2, 6).unwrap();
255 g.add_edge(3, 7).unwrap();
256 g.add_edge(3, 8).unwrap();
257 g.add_edge(4, 9).unwrap();
258 g.add_edge(4, 10).unwrap();
259 g.add_edge(5, 11).unwrap();
260 g.add_edge(5, 12).unwrap();
261 g.add_edge(6, 13).unwrap();
262 g.add_edge(6, 14).unwrap();
263 assert!(is_lobster(&g).unwrap());
264 }
265
266 #[test]
267 fn binary_tree_depth4_not_lobster() {
268 // Complete binary tree of depth 4 (31 vertices).
269 // After removing leaves twice, the remainder has branching → not lobster.
270 let mut g = Graph::with_vertices(31);
271 for i in 0..15u32 {
272 g.add_edge(i, 2 * i + 1).unwrap();
273 g.add_edge(i, 2 * i + 2).unwrap();
274 }
275 assert!(!is_lobster(&g).unwrap());
276 }
277}