Skip to main content

rust_igraph/algorithms/properties/
is_forest.rs

1//! Forest predicate (ALGO-PR-024).
2//!
3//! Counterpart of `igraph_is_forest()` from
4//! `references/igraph/src/properties/trees.c:520`. Returns
5//! `Some(roots)` iff `graph` is a forest under `mode`, otherwise
6//! `None`. Unlike [`crate::is_tree`], the null graph (`vcount == 0`)
7//! **is** considered a forest by convention (with an empty root
8//! list).
9//!
10//! For directed graphs the [`DijkstraMode`] argument selects the
11//! orientation:
12//! - [`DijkstraMode::Out`]: out-forest — every tree component is an
13//!   out-arborescence; roots are vertices with in-degree `0`.
14//! - [`DijkstraMode::In`]: in-forest — every tree component is an
15//!   in-arborescence; roots are vertices with out-degree `0`.
16//! - [`DijkstraMode::All`]: edge directions ignored; roots are
17//!   the canonical (lowest-id) vertex of each connected component.
18//!
19//! For undirected graphs the mode argument is ignored (matches
20//! upstream).
21//!
22//! Time complexity: `O(V + E)`.
23//!
24//! Note: `is_forest` and [`crate::is_acyclic`] differ for directed
25//! graphs. `is_acyclic` only forbids *directed* cycles, whereas
26//! `is_forest(_, Out)` additionally forbids in-degree `> 1` (i.e.
27//! a vertex receiving from two parents — an *undirected* cycle).
28//! Example: `0 → 2 ← 1` is acyclic but is **not** an out-forest
29//! because vertex 2 has in-degree 2.
30
31use crate::algorithms::paths::dijkstra::DijkstraMode;
32use crate::core::Graph;
33use crate::core::cache::CachedProperty;
34use crate::core::error::IgraphResult;
35use crate::core::graph::{EdgeId, VertexId};
36
37/// Returns `Some(roots)` iff `graph` is a forest under `mode`,
38/// otherwise `None`. The null graph is a forest with empty roots.
39///
40/// For undirected graphs the mode argument is ignored. For directed
41/// graphs:
42/// - [`DijkstraMode::Out`]: roots are the vertices with in-degree
43///   `0` (one per tree component).
44/// - [`DijkstraMode::In`]: roots are the vertices with out-degree
45///   `0` (one per tree component).
46/// - [`DijkstraMode::All`]: orientation ignored; the canonical root
47///   of each component is the lowest-indexed unvisited vertex
48///   reached during a left-to-right scan.
49///
50/// Counterpart of `igraph_is_forest(_, _, _, mode)` from
51/// `references/igraph/src/properties/trees.c:520`.
52///
53/// # Examples
54///
55/// ```
56/// use rust_igraph::{Graph, is_forest, DijkstraMode};
57///
58/// // Two disjoint undirected edges: 0-1 and 2-3 form a forest.
59/// let mut g = Graph::with_vertices(4);
60/// g.add_edges(vec![(0u32, 1u32), (2, 3)]).unwrap();
61/// let roots = is_forest(&g, DijkstraMode::All).unwrap().unwrap();
62/// assert_eq!(roots, vec![0, 2]);
63///
64/// // Triangle 0-1-2-0 has a cycle ⇒ not a forest.
65/// let mut g = Graph::with_vertices(3);
66/// g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 0)]).unwrap();
67/// assert!(is_forest(&g, DijkstraMode::All).unwrap().is_none());
68///
69/// // Two disjoint out-arborescences 0→1 and 2→3.
70/// let mut g = Graph::new(4, true).unwrap();
71/// g.add_edges(vec![(0u32, 1u32), (2, 3)]).unwrap();
72/// let roots = is_forest(&g, DijkstraMode::Out).unwrap().unwrap();
73/// assert_eq!(roots, vec![0, 2]);
74///
75/// // 0→2←1: not an out-forest (in-degree 2 at vertex 2),
76/// // but is an undirected forest (treated as 0-2-1 path).
77/// let mut g = Graph::new(3, true).unwrap();
78/// g.add_edges(vec![(0u32, 2u32), (1, 2)]).unwrap();
79/// assert!(is_forest(&g, DijkstraMode::Out).unwrap().is_none());
80/// assert!(is_forest(&g, DijkstraMode::All).unwrap().is_some());
81/// ```
82pub fn is_forest(graph: &Graph, mode: DijkstraMode) -> IgraphResult<Option<Vec<VertexId>>> {
83    let n = graph.vcount();
84    let m = graph.ecount();
85
86    // Null graph: forest with empty roots.
87    if n == 0 {
88        if matches!(
89            (graph.is_directed(), mode),
90            (false, _) | (true, DijkstraMode::All)
91        ) {
92            graph.cache_set(CachedProperty::IsForest, true);
93        }
94        return Ok(Some(Vec::new()));
95    }
96
97    // No edges: every vertex is its own tree, all are roots.
98    if m == 0 {
99        if matches!(
100            (graph.is_directed(), mode),
101            (false, _) | (true, DijkstraMode::All)
102        ) {
103            graph.cache_set(CachedProperty::IsForest, true);
104        }
105        return Ok(Some((0..n).collect()));
106    }
107
108    let directed = graph.is_directed();
109    let effective_mode = if directed { mode } else { DijkstraMode::All };
110    let cache_eligible = matches!(effective_mode, DijkstraMode::All);
111
112    if cache_eligible {
113        if let Some(false) = graph.cache_get(CachedProperty::IsForest) {
114            return Ok(None);
115        }
116    }
117
118    // Forest has at most n-1 edges.
119    if m as u64 > u64::from(n).saturating_sub(1) {
120        if cache_eligible {
121            graph.cache_set(CachedProperty::IsForest, false);
122        }
123        return Ok(None);
124    }
125
126    let n_us = n as usize;
127    let mut visited = vec![false; n_us];
128    let mut visited_count: u32 = 0;
129
130    let roots_opt = collect_roots_and_traverse(
131        graph,
132        n,
133        effective_mode,
134        directed,
135        &mut visited,
136        &mut visited_count,
137    )?;
138
139    let Some(roots) = roots_opt else {
140        if cache_eligible {
141            graph.cache_set(CachedProperty::IsForest, false);
142        }
143        return Ok(None);
144    };
145
146    // All vertices must be reachable from some root.
147    if visited_count == n {
148        if cache_eligible {
149            graph.cache_set(CachedProperty::IsForest, true);
150        }
151        Ok(Some(roots))
152    } else {
153        if cache_eligible {
154            graph.cache_set(CachedProperty::IsForest, false);
155        }
156        Ok(None)
157    }
158}
159
160/// Iterate vertices in id order, picking roots per `mode` and
161/// running an `is_forest_visitor` DFS from each. Returns `None`
162/// the moment a cycle, an arity violation, or a self-loop is
163/// detected.
164fn collect_roots_and_traverse(
165    graph: &Graph,
166    n: VertexId,
167    mode: DijkstraMode,
168    directed: bool,
169    visited: &mut [bool],
170    visited_count: &mut u32,
171) -> IgraphResult<Option<Vec<VertexId>>> {
172    let mut roots: Vec<VertexId> = Vec::new();
173
174    match mode {
175        DijkstraMode::All => {
176            for v in 0..n {
177                if !visited[v as usize] {
178                    roots.push(v);
179                    if !is_forest_visitor(graph, v, mode, directed, visited, visited_count)? {
180                        return Ok(None);
181                    }
182                }
183            }
184        }
185        DijkstraMode::Out | DijkstraMode::In => {
186            // For an out-tree, every vertex has in-degree ≤ 1
187            // (roots: 0). For an in-tree, every vertex has
188            // out-degree ≤ 1 (roots: 0). Counter-direction degree.
189            let use_in_degree = matches!(mode, DijkstraMode::Out);
190            for v in 0..n {
191                let d = counter_degree(graph, v, use_in_degree)?;
192                if d > 1 {
193                    return Ok(None);
194                }
195                if d == 0 {
196                    roots.push(v);
197                    if !is_forest_visitor(graph, v, mode, directed, visited, visited_count)? {
198                        return Ok(None);
199                    }
200                }
201            }
202        }
203    }
204
205    Ok(Some(roots))
206}
207
208/// Counter-direction degree (in-degree if `use_in_degree`, else
209/// out-degree). Counts every incidence including parallel edges
210/// and self-loops once per stored entry; this matches upstream's
211/// `igraph_degree(_, IGRAPH_REVERSE_MODE(mode), IGRAPH_LOOPS)`
212/// behaviour.
213fn counter_degree(graph: &Graph, v: VertexId, use_in_degree: bool) -> IgraphResult<usize> {
214    if use_in_degree {
215        Ok(graph.incident_in(v)?.len())
216    } else {
217        Ok(graph.incident(v)?.len())
218    }
219}
220
221/// DFS from `root` in the given `mode`. Returns `false` if a cycle
222/// (or self-loop in undirected) is detected; `true` otherwise. On
223/// the way it sets `visited[]` and increments `visited_count`.
224///
225/// Mirrors `igraph_i_is_forest_visitor` from
226/// `references/igraph/src/properties/trees.c:402`.
227fn is_forest_visitor(
228    graph: &Graph,
229    root: VertexId,
230    mode: DijkstraMode,
231    directed: bool,
232    visited: &mut [bool],
233    visited_count: &mut u32,
234) -> IgraphResult<bool> {
235    let mut stack: Vec<VertexId> = Vec::new();
236    stack.push(root);
237
238    while let Some(u) = stack.pop() {
239        if visited[u as usize] {
240            // We popped a vertex that was already marked: we
241            // re-entered it through a back edge (non-tree edge).
242            // That's a cycle.
243            return Ok(false);
244        }
245        visited[u as usize] = true;
246        *visited_count = visited_count.saturating_add(1);
247
248        for eid in incident_edges(graph, u, mode, directed)? {
249            let nei = graph.edge_other(eid, u)?;
250            match mode {
251                DijkstraMode::All => {
252                    if !visited[nei as usize] {
253                        stack.push(nei);
254                    } else if nei == u {
255                        // self-loop in the undirected case
256                        return Ok(false);
257                    }
258                    // else: skip the parent edge.
259                }
260                DijkstraMode::Out | DijkstraMode::In => {
261                    // Push every neighbour; cycle is detected by
262                    // popping a vertex that's already visited.
263                    stack.push(nei);
264                }
265            }
266        }
267    }
268
269    Ok(true)
270}
271
272/// Edges incident on `u` in the requested orientation. (Re-uses
273/// the same dispatch table as `is_tree`.)
274fn incident_edges(
275    graph: &Graph,
276    u: VertexId,
277    mode: DijkstraMode,
278    directed: bool,
279) -> IgraphResult<Vec<EdgeId>> {
280    match mode {
281        DijkstraMode::Out => graph.incident(u),
282        DijkstraMode::In => graph.incident_in(u),
283        DijkstraMode::All => {
284            if directed {
285                let mut out = graph.incident(u)?;
286                out.extend(graph.incident_in(u)?);
287                Ok(out)
288            } else {
289                graph.incident(u)
290            }
291        }
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    // -------- Null / no-edge --------
300
301    #[test]
302    fn null_graph_is_forest_with_empty_roots() {
303        let g = Graph::with_vertices(0);
304        let roots = is_forest(&g, DijkstraMode::All).unwrap().unwrap();
305        assert!(roots.is_empty());
306    }
307
308    #[test]
309    fn no_edges_undirected_every_vertex_is_a_root() {
310        let g = Graph::with_vertices(4);
311        let roots = is_forest(&g, DijkstraMode::All).unwrap().unwrap();
312        assert_eq!(roots, vec![0, 1, 2, 3]);
313    }
314
315    #[test]
316    fn no_edges_directed_out_every_vertex_is_a_root() {
317        let g = Graph::new(3, true).unwrap();
318        let roots = is_forest(&g, DijkstraMode::Out).unwrap().unwrap();
319        assert_eq!(roots, vec![0, 1, 2]);
320    }
321
322    // -------- Undirected --------
323
324    #[test]
325    fn undirected_path_is_forest_one_root() {
326        let mut g = Graph::with_vertices(4);
327        g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3)]).unwrap();
328        let roots = is_forest(&g, DijkstraMode::All).unwrap().unwrap();
329        assert_eq!(roots, vec![0]);
330    }
331
332    #[test]
333    fn undirected_two_components_each_a_tree() {
334        let mut g = Graph::with_vertices(5);
335        g.add_edges(vec![(0u32, 1u32), (2, 3), (3, 4)]).unwrap();
336        let roots = is_forest(&g, DijkstraMode::All).unwrap().unwrap();
337        assert_eq!(roots, vec![0, 2]);
338    }
339
340    #[test]
341    fn undirected_triangle_is_not_a_forest() {
342        let mut g = Graph::with_vertices(3);
343        g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 0)]).unwrap();
344        assert!(is_forest(&g, DijkstraMode::All).unwrap().is_none());
345    }
346
347    #[test]
348    fn undirected_self_loop_breaks_forest() {
349        // (0,0) self-loop on vertex 0; rest is forest.
350        let mut g = Graph::with_vertices(3);
351        g.add_edge(0, 0).unwrap();
352        g.add_edge(1, 2).unwrap();
353        // ecount=2, vcount=3 ⇒ ecount ≤ vcount-1 holds. But the
354        // self-loop is detected during the DFS visitor.
355        assert!(is_forest(&g, DijkstraMode::All).unwrap().is_none());
356    }
357
358    #[test]
359    fn undirected_parallel_edges_break_forest() {
360        // Two parallel edges 0-1 form a 2-cycle.
361        let mut g = Graph::with_vertices(2);
362        g.add_edge(0, 1).unwrap();
363        g.add_edge(0, 1).unwrap();
364        // ecount=2 > vcount-1=1 ⇒ failed by the cardinality bound.
365        assert!(is_forest(&g, DijkstraMode::All).unwrap().is_none());
366    }
367
368    // -------- Directed: OUT --------
369
370    #[test]
371    fn directed_out_two_arborescences_are_a_forest() {
372        let mut g = Graph::new(4, true).unwrap();
373        g.add_edges(vec![(0u32, 1u32), (2, 3)]).unwrap();
374        let roots = is_forest(&g, DijkstraMode::Out).unwrap().unwrap();
375        assert_eq!(roots, vec![0, 2]);
376    }
377
378    #[test]
379    fn directed_out_v_pattern_is_not_an_out_forest() {
380        // 0→2, 1→2: vertex 2 has in-degree 2.
381        let mut g = Graph::new(3, true).unwrap();
382        g.add_edges(vec![(0u32, 2u32), (1, 2)]).unwrap();
383        assert!(is_forest(&g, DijkstraMode::Out).unwrap().is_none());
384    }
385
386    #[test]
387    fn directed_in_two_anti_arborescences_are_a_forest() {
388        // 1→0, 3→2: every edge points to a sink.
389        let mut g = Graph::new(4, true).unwrap();
390        g.add_edges(vec![(1u32, 0u32), (3, 2)]).unwrap();
391        let roots = is_forest(&g, DijkstraMode::In).unwrap().unwrap();
392        assert_eq!(roots, vec![0, 2]);
393    }
394
395    #[test]
396    fn directed_cycle_is_not_a_forest_in_any_mode() {
397        let mut g = Graph::new(3, true).unwrap();
398        g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 0)]).unwrap();
399        assert!(is_forest(&g, DijkstraMode::Out).unwrap().is_none());
400        assert!(is_forest(&g, DijkstraMode::In).unwrap().is_none());
401        assert!(is_forest(&g, DijkstraMode::All).unwrap().is_none());
402    }
403
404    #[test]
405    fn directed_all_mode_treats_as_undirected() {
406        // 0→2←1 — not an out-forest, but undirected it's a path.
407        let mut g = Graph::new(3, true).unwrap();
408        g.add_edges(vec![(0u32, 2u32), (1, 2)]).unwrap();
409        assert!(is_forest(&g, DijkstraMode::Out).unwrap().is_none());
410        let roots = is_forest(&g, DijkstraMode::All).unwrap().unwrap();
411        assert_eq!(roots, vec![0]);
412    }
413
414    // -------- Edge-cardinality fast path --------
415
416    #[test]
417    fn too_many_edges_is_not_a_forest() {
418        // 5 vertices, 5 edges ⇒ ecount > vcount-1 ⇒ not forest.
419        let mut g = Graph::with_vertices(5);
420        g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3), (3, 4), (4, 0)])
421            .unwrap();
422        assert!(is_forest(&g, DijkstraMode::All).unwrap().is_none());
423    }
424
425    #[test]
426    fn correct_edge_count_but_disconnected_with_cycle_is_not_a_forest() {
427        // vcount=4, ecount=3=vcount-1. Edges (0,1),(1,0),(2,3) ⇒
428        // multi-edge between 0 and 1 forms a cycle, vertex 3
429        // remains reachable from 2.
430        let mut g = Graph::with_vertices(4);
431        g.add_edge(0, 1).unwrap();
432        g.add_edge(0, 1).unwrap();
433        g.add_edge(2, 3).unwrap();
434        assert!(is_forest(&g, DijkstraMode::All).unwrap().is_none());
435    }
436
437    // -------- Single tree is also a forest --------
438
439    #[test]
440    fn single_tree_is_also_a_forest() {
441        let mut g = Graph::with_vertices(4);
442        g.add_edges(vec![(0u32, 1u32), (0, 2), (0, 3)]).unwrap();
443        let roots = is_forest(&g, DijkstraMode::All).unwrap().unwrap();
444        assert_eq!(roots, vec![0]);
445    }
446
447    #[test]
448    fn single_out_arborescence_is_also_an_out_forest() {
449        let mut g = Graph::new(4, true).unwrap();
450        g.add_edges(vec![(0u32, 1u32), (0, 2), (1, 3)]).unwrap();
451        let roots = is_forest(&g, DijkstraMode::Out).unwrap().unwrap();
452        assert_eq!(roots, vec![0]);
453    }
454}