Skip to main content

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}