Skip to main content

rust_igraph/algorithms/properties/
is_apex_forest.rs

1//! Apex forest predicate (ALGO-PR-120).
2//!
3//! A graph is an apex forest if there exists a vertex whose removal
4//! makes the remaining graph a forest (acyclic). Equivalently, all
5//! cycles in the graph pass through a common vertex.
6//!
7//! Every forest is trivially an apex forest (remove any vertex).
8//! Every graph with a single cycle passing through one vertex is
9//! an apex forest.
10//!
11//! Directed graphs are treated as undirected.
12
13use crate::algorithms::paths::dijkstra::DijkstraMode;
14use crate::algorithms::properties::is_forest::is_forest;
15use crate::core::{Graph, IgraphResult};
16
17/// Check whether a graph is an apex forest.
18///
19/// A graph is an apex forest if removing some single vertex yields a
20/// forest. Tests each vertex as a candidate apex.
21///
22/// Directed graphs are treated as undirected.
23///
24/// # Examples
25///
26/// ```
27/// use rust_igraph::{Graph, is_apex_forest};
28///
29/// // Triangle: remove any vertex → single edge (forest) ✓
30/// let mut g = Graph::with_vertices(3);
31/// g.add_edge(0, 1).unwrap();
32/// g.add_edge(1, 2).unwrap();
33/// g.add_edge(2, 0).unwrap();
34/// assert!(is_apex_forest(&g).unwrap());
35///
36/// // Two disjoint triangles: no single vertex removal yields forest
37/// let mut g = Graph::with_vertices(6);
38/// g.add_edge(0, 1).unwrap();
39/// g.add_edge(1, 2).unwrap();
40/// g.add_edge(2, 0).unwrap();
41/// g.add_edge(3, 4).unwrap();
42/// g.add_edge(4, 5).unwrap();
43/// g.add_edge(5, 3).unwrap();
44/// assert!(!is_apex_forest(&g).unwrap());
45/// ```
46pub fn is_apex_forest(graph: &Graph) -> IgraphResult<bool> {
47    let n = graph.vcount();
48
49    if n == 0 {
50        return Ok(true);
51    }
52
53    if is_forest(graph, DijkstraMode::All)?.is_some() {
54        return Ok(true);
55    }
56
57    let n_usize = n as usize;
58    let mut adj = vec![vec![false; n_usize]; n_usize];
59    for v in 0..n {
60        let nbrs = graph.neighbors(v)?;
61        for &w in &nbrs {
62            adj[v as usize][w as usize] = true;
63            adj[w as usize][v as usize] = true;
64        }
65    }
66
67    for apex in 0..n_usize {
68        if is_forest_without(&adj, apex, n_usize) {
69            return Ok(true);
70        }
71    }
72
73    Ok(false)
74}
75
76/// Check if the graph minus vertex `removed` is a forest.
77fn is_forest_without(adj: &[Vec<bool>], removed: usize, n: usize) -> bool {
78    let mut visited = vec![false; n];
79    visited[removed] = true;
80
81    let remaining = n - 1;
82    if remaining == 0 {
83        return true;
84    }
85
86    let mut edge_count = 0usize;
87    let mut component_count = 0usize;
88    let mut visited_count = 0usize;
89
90    for start in 0..n {
91        if visited[start] {
92            continue;
93        }
94        component_count += 1;
95        let mut stack = vec![start];
96        visited[start] = true;
97
98        while let Some(u) = stack.pop() {
99            visited_count += 1;
100            for (v, &is_adj) in adj[u].iter().enumerate() {
101                if v == removed || !is_adj {
102                    continue;
103                }
104                edge_count += 1;
105                if !visited[v] {
106                    visited[v] = true;
107                    stack.push(v);
108                }
109            }
110        }
111    }
112
113    // Each edge counted twice (u→v and v→u)
114    edge_count /= 2;
115
116    // Forest iff edges == vertices - components
117    edge_count == visited_count - component_count
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn empty_graph() {
126        let g = Graph::with_vertices(0);
127        assert!(is_apex_forest(&g).unwrap());
128    }
129
130    #[test]
131    fn single_vertex() {
132        let g = Graph::with_vertices(1);
133        assert!(is_apex_forest(&g).unwrap());
134    }
135
136    #[test]
137    fn single_edge() {
138        let mut g = Graph::with_vertices(2);
139        g.add_edge(0, 1).unwrap();
140        assert!(is_apex_forest(&g).unwrap());
141    }
142
143    #[test]
144    fn triangle() {
145        let mut g = Graph::with_vertices(3);
146        g.add_edge(0, 1).unwrap();
147        g.add_edge(1, 2).unwrap();
148        g.add_edge(2, 0).unwrap();
149        assert!(is_apex_forest(&g).unwrap());
150    }
151
152    #[test]
153    fn tree() {
154        let mut g = Graph::with_vertices(5);
155        g.add_edge(0, 1).unwrap();
156        g.add_edge(1, 2).unwrap();
157        g.add_edge(1, 3).unwrap();
158        g.add_edge(3, 4).unwrap();
159        assert!(is_apex_forest(&g).unwrap());
160    }
161
162    #[test]
163    fn c4() {
164        // C_4: remove any vertex → P_3 (forest) ✓
165        let mut g = Graph::with_vertices(4);
166        g.add_edge(0, 1).unwrap();
167        g.add_edge(1, 2).unwrap();
168        g.add_edge(2, 3).unwrap();
169        g.add_edge(3, 0).unwrap();
170        assert!(is_apex_forest(&g).unwrap());
171    }
172
173    #[test]
174    fn k4() {
175        // K_4: remove any vertex → K_3 (triangle, NOT a forest).
176        // So K_4 is NOT an apex forest.
177        let mut g = Graph::with_vertices(4);
178        for i in 0..4u32 {
179            for j in (i + 1)..4 {
180                g.add_edge(i, j).unwrap();
181            }
182        }
183        assert!(!is_apex_forest(&g).unwrap());
184    }
185
186    #[test]
187    fn two_triangles_not_apex() {
188        let mut g = Graph::with_vertices(6);
189        g.add_edge(0, 1).unwrap();
190        g.add_edge(1, 2).unwrap();
191        g.add_edge(2, 0).unwrap();
192        g.add_edge(3, 4).unwrap();
193        g.add_edge(4, 5).unwrap();
194        g.add_edge(5, 3).unwrap();
195        assert!(!is_apex_forest(&g).unwrap());
196    }
197
198    #[test]
199    fn wheel_w4() {
200        // W_4: center 0, rim 1-2-3-4-1.
201        // Remove center → C_4 (has cycle) → not forest.
202        // Remove rim vertex 1 → 0 connected to 2,3,4; plus 2-3,3-4.
203        // Edges: 0-2,0-3,0-4,2-3,3-4. Has cycle 0-2-3-0 → not forest.
204        // K_4 subgraph minus one edge... still has cycle.
205        // Actually let's check: remove 1 from W_4.
206        // Remaining: 0,2,3,4 with edges 0-2,0-3,0-4,2-3,3-4.
207        // 4 vertices, 5 edges. Forest needs 3 edges. NOT forest.
208        // W_4 is NOT apex forest.
209        let mut g = Graph::with_vertices(5);
210        g.add_edge(0, 1).unwrap();
211        g.add_edge(0, 2).unwrap();
212        g.add_edge(0, 3).unwrap();
213        g.add_edge(0, 4).unwrap();
214        g.add_edge(1, 2).unwrap();
215        g.add_edge(2, 3).unwrap();
216        g.add_edge(3, 4).unwrap();
217        g.add_edge(4, 1).unwrap();
218        assert!(!is_apex_forest(&g).unwrap());
219    }
220
221    #[test]
222    fn diamond_apex() {
223        // Diamond: 0-1,0-2,0-3,1-2,1-3 (K_4 minus 2-3).
224        // Remove 0: remaining 1,2,3 with edges 1-2,1-3 → star → forest ✓.
225        let mut g = Graph::with_vertices(4);
226        g.add_edge(0, 1).unwrap();
227        g.add_edge(0, 2).unwrap();
228        g.add_edge(0, 3).unwrap();
229        g.add_edge(1, 2).unwrap();
230        g.add_edge(1, 3).unwrap();
231        assert!(is_apex_forest(&g).unwrap());
232    }
233
234    #[test]
235    fn theta_graph_apex() {
236        // Theta: two paths between 0 and 3: 0-1-3 and 0-2-3.
237        // Remove 0 → 1,2,3 with edges 1-3,2-3 → forest ✓.
238        let mut g = Graph::with_vertices(4);
239        g.add_edge(0, 1).unwrap();
240        g.add_edge(1, 3).unwrap();
241        g.add_edge(0, 2).unwrap();
242        g.add_edge(2, 3).unwrap();
243        assert!(is_apex_forest(&g).unwrap());
244    }
245
246    #[test]
247    fn edgeless() {
248        let g = Graph::with_vertices(4);
249        assert!(is_apex_forest(&g).unwrap());
250    }
251
252    #[test]
253    fn directed_treated_as_undirected() {
254        let mut g = Graph::new(3, true).unwrap();
255        g.add_edge(0, 1).unwrap();
256        g.add_edge(1, 2).unwrap();
257        g.add_edge(2, 0).unwrap();
258        assert!(is_apex_forest(&g).unwrap());
259    }
260
261    #[test]
262    fn two_triangles_shared_vertex() {
263        // 0-1-2-0 and 0-3-4-0. All cycles through 0.
264        // Remove 0 → 1-2, 3-4 (two edges, forest) ✓.
265        let mut g = Graph::with_vertices(5);
266        g.add_edge(0, 1).unwrap();
267        g.add_edge(1, 2).unwrap();
268        g.add_edge(2, 0).unwrap();
269        g.add_edge(0, 3).unwrap();
270        g.add_edge(3, 4).unwrap();
271        g.add_edge(4, 0).unwrap();
272        assert!(is_apex_forest(&g).unwrap());
273    }
274}