Skip to main content

rust_igraph/algorithms/properties/
is_pseudo_forest.rs

1//! Pseudo-forest predicate (ALGO-PR-075).
2//!
3//! A pseudo-forest is an undirected graph where every connected
4//! component contains at most one cycle. Equivalently, for each
5//! connected component, |E| <= |V|.
6//!
7//! A forest (|E| = |V| - 1 per component) is a special case.
8//! A 1-tree (unicyclic component, |E| = |V|) is a pseudo-forest
9//! component with exactly one cycle.
10//!
11//! For directed graphs, the function returns `false` (the concept
12//! is defined for undirected graphs).
13
14use crate::algorithms::connectivity::components::connected_components;
15use crate::core::{Graph, IgraphResult};
16
17/// Check whether a graph is a pseudo-forest.
18///
19/// A pseudo-forest has at most one cycle per connected component.
20/// This is equivalent to requiring |E| <= |V| for every component.
21///
22/// Returns `false` for directed graphs.
23///
24/// An empty graph (0 vertices) is a pseudo-forest (vacuously).
25/// An edgeless graph is a pseudo-forest (forest with no edges).
26/// A single cycle is a pseudo-forest.
27///
28/// # Examples
29///
30/// ```
31/// use rust_igraph::{Graph, is_pseudo_forest};
32///
33/// // Triangle: one component with |E|=3, |V|=3 → pseudo-forest
34/// let mut g = Graph::with_vertices(3);
35/// g.add_edge(0, 1).unwrap();
36/// g.add_edge(1, 2).unwrap();
37/// g.add_edge(2, 0).unwrap();
38/// assert!(is_pseudo_forest(&g).unwrap());
39///
40/// // K4: |E|=6, |V|=4 → NOT pseudo-forest
41/// let mut g = Graph::with_vertices(4);
42/// for u in 0..4u32 {
43///     for v in (u + 1)..4 {
44///         g.add_edge(u, v).unwrap();
45///     }
46/// }
47/// assert!(!is_pseudo_forest(&g).unwrap());
48/// ```
49pub fn is_pseudo_forest(graph: &Graph) -> IgraphResult<bool> {
50    if graph.is_directed() {
51        return Ok(false);
52    }
53
54    let n = graph.vcount();
55    if n == 0 {
56        return Ok(true);
57    }
58
59    let cc = connected_components(graph)?;
60    let comp_count = cc.count as usize;
61
62    let mut comp_verts = vec![0u64; comp_count];
63    let mut comp_edges = vec![0u64; comp_count];
64
65    for v in 0..n {
66        let c = cc.membership[v as usize] as usize;
67        comp_verts[c] = comp_verts[c].saturating_add(1);
68    }
69
70    let ecount = graph.ecount();
71    for eid in 0..ecount {
72        let eid_u32 = u32::try_from(eid).unwrap_or(u32::MAX);
73        let (u, _v) = graph.edge(eid_u32)?;
74        let c = cc.membership[u as usize] as usize;
75        comp_edges[c] = comp_edges[c].saturating_add(1);
76    }
77
78    for c in 0..comp_count {
79        if comp_edges[c] > comp_verts[c] {
80            return Ok(false);
81        }
82    }
83
84    Ok(true)
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn empty_graph() {
93        let g = Graph::with_vertices(0);
94        assert!(is_pseudo_forest(&g).unwrap());
95    }
96
97    #[test]
98    fn single_vertex() {
99        let g = Graph::with_vertices(1);
100        assert!(is_pseudo_forest(&g).unwrap());
101    }
102
103    #[test]
104    fn edgeless_graph() {
105        let g = Graph::with_vertices(5);
106        assert!(is_pseudo_forest(&g).unwrap());
107    }
108
109    #[test]
110    fn single_edge() {
111        let mut g = Graph::with_vertices(2);
112        g.add_edge(0, 1).unwrap();
113        assert!(is_pseudo_forest(&g).unwrap());
114    }
115
116    #[test]
117    fn path_is_pseudo_forest() {
118        let mut g = Graph::with_vertices(4);
119        g.add_edge(0, 1).unwrap();
120        g.add_edge(1, 2).unwrap();
121        g.add_edge(2, 3).unwrap();
122        assert!(is_pseudo_forest(&g).unwrap());
123    }
124
125    #[test]
126    fn tree_is_pseudo_forest() {
127        let mut g = Graph::with_vertices(5);
128        g.add_edge(0, 1).unwrap();
129        g.add_edge(0, 2).unwrap();
130        g.add_edge(1, 3).unwrap();
131        g.add_edge(1, 4).unwrap();
132        assert!(is_pseudo_forest(&g).unwrap());
133    }
134
135    #[test]
136    fn triangle_is_pseudo_forest() {
137        let mut g = Graph::with_vertices(3);
138        g.add_edge(0, 1).unwrap();
139        g.add_edge(1, 2).unwrap();
140        g.add_edge(2, 0).unwrap();
141        assert!(is_pseudo_forest(&g).unwrap());
142    }
143
144    #[test]
145    fn cycle_4_is_pseudo_forest() {
146        let mut g = Graph::with_vertices(4);
147        g.add_edge(0, 1).unwrap();
148        g.add_edge(1, 2).unwrap();
149        g.add_edge(2, 3).unwrap();
150        g.add_edge(3, 0).unwrap();
151        assert!(is_pseudo_forest(&g).unwrap());
152    }
153
154    #[test]
155    fn unicyclic_with_tail() {
156        // Triangle 0-1-2 + tail 2-3-4: |V|=5, |E|=4 per component → ok
157        let mut g = Graph::with_vertices(5);
158        g.add_edge(0, 1).unwrap();
159        g.add_edge(1, 2).unwrap();
160        g.add_edge(2, 0).unwrap();
161        g.add_edge(2, 3).unwrap();
162        g.add_edge(3, 4).unwrap();
163        assert!(is_pseudo_forest(&g).unwrap());
164    }
165
166    #[test]
167    fn two_disjoint_cycles() {
168        // Two triangles as separate components: each has |E|=|V|=3
169        let mut g = Graph::with_vertices(6);
170        g.add_edge(0, 1).unwrap();
171        g.add_edge(1, 2).unwrap();
172        g.add_edge(2, 0).unwrap();
173        g.add_edge(3, 4).unwrap();
174        g.add_edge(4, 5).unwrap();
175        g.add_edge(5, 3).unwrap();
176        assert!(is_pseudo_forest(&g).unwrap());
177    }
178
179    #[test]
180    fn k4_not_pseudo_forest() {
181        // K4: |E|=6, |V|=4 → two cycles → not pseudo-forest
182        let mut g = Graph::with_vertices(4);
183        for u in 0..4u32 {
184            for v in (u + 1)..4 {
185                g.add_edge(u, v).unwrap();
186            }
187        }
188        assert!(!is_pseudo_forest(&g).unwrap());
189    }
190
191    #[test]
192    fn diamond_not_pseudo_forest() {
193        // Diamond = K4 - edge: |V|=4, |E|=5 → not pseudo-forest
194        let mut g = Graph::with_vertices(4);
195        g.add_edge(0, 1).unwrap();
196        g.add_edge(0, 2).unwrap();
197        g.add_edge(0, 3).unwrap();
198        g.add_edge(1, 2).unwrap();
199        g.add_edge(1, 3).unwrap();
200        assert!(!is_pseudo_forest(&g).unwrap());
201    }
202
203    #[test]
204    fn theta_graph_not_pseudo_forest() {
205        // Theta: two vertices connected by 3 internally disjoint paths
206        // e.g., 0-1, 0-2-1, 0-3-1: |V|=4, |E|=5 → not pseudo-forest
207        let mut g = Graph::with_vertices(4);
208        g.add_edge(0, 1).unwrap();
209        g.add_edge(0, 2).unwrap();
210        g.add_edge(2, 1).unwrap();
211        g.add_edge(0, 3).unwrap();
212        g.add_edge(3, 1).unwrap();
213        assert!(!is_pseudo_forest(&g).unwrap());
214    }
215
216    #[test]
217    fn directed_returns_false() {
218        let mut g = Graph::new(3, true).unwrap();
219        g.add_edge(0, 1).unwrap();
220        g.add_edge(1, 2).unwrap();
221        assert!(!is_pseudo_forest(&g).unwrap());
222    }
223
224    #[test]
225    fn self_loop_counts() {
226        // Self-loop: component has |V|=1, |E|=1 → ok (one cycle)
227        let mut g = Graph::with_vertices(1);
228        g.add_edge(0, 0).unwrap();
229        assert!(is_pseudo_forest(&g).unwrap());
230    }
231
232    #[test]
233    fn two_self_loops_not_pseudo_forest() {
234        // Two self-loops on same vertex: |V|=1, |E|=2 → not pseudo-forest
235        let mut g = Graph::with_vertices(1);
236        g.add_edge(0, 0).unwrap();
237        g.add_edge(0, 0).unwrap();
238        assert!(!is_pseudo_forest(&g).unwrap());
239    }
240
241    #[test]
242    fn parallel_edges_not_pseudo_forest() {
243        // Two parallel edges + one more: |V|=2, |E|=3 → not pseudo-forest
244        let mut g = Graph::with_vertices(2);
245        g.add_edge(0, 1).unwrap();
246        g.add_edge(0, 1).unwrap();
247        g.add_edge(0, 1).unwrap();
248        assert!(!is_pseudo_forest(&g).unwrap());
249    }
250
251    #[test]
252    fn parallel_edge_is_pseudo_forest() {
253        // Two parallel edges: |V|=2, |E|=2 → pseudo-forest (one cycle)
254        let mut g = Graph::with_vertices(2);
255        g.add_edge(0, 1).unwrap();
256        g.add_edge(0, 1).unwrap();
257        assert!(is_pseudo_forest(&g).unwrap());
258    }
259
260    #[test]
261    fn mixed_components() {
262        // Component 1: tree {0,1,2} with edges 0-1, 1-2 (|E|=2, |V|=3 → ok)
263        // Component 2: K4 {3,4,5,6} (|E|=6, |V|=4 → not ok)
264        let mut g = Graph::with_vertices(7);
265        g.add_edge(0, 1).unwrap();
266        g.add_edge(1, 2).unwrap();
267        for u in 3..7u32 {
268            for v in (u + 1)..7 {
269                g.add_edge(u, v).unwrap();
270            }
271        }
272        assert!(!is_pseudo_forest(&g).unwrap());
273    }
274}