Skip to main content

rust_igraph/algorithms/properties/
is_tree.rs

1//! Tree predicate (ALGO-PR-023).
2//!
3//! Counterpart of `igraph_is_tree()` from
4//! `references/igraph/src/properties/trees.c:251`. Returns
5//! `Some(root)` iff `graph` is a tree (a connected, acyclic graph),
6//! and `None` otherwise. The null graph (`vcount == 0`) is by
7//! convention **not** a tree.
8//!
9//! For directed graphs the [`DijkstraMode`] argument selects the
10//! orientation:
11//! - [`DijkstraMode::Out`]: out-tree / arborescence — every edge
12//!   points away from the root.
13//! - [`DijkstraMode::In`]: in-tree / anti-arborescence — every edge
14//!   points towards the root.
15//! - [`DijkstraMode::All`]: ignore orientation; checks the
16//!   underlying undirected structure.
17//!
18//! For undirected graphs the mode argument is ignored (matches
19//! upstream).
20//!
21//! Time complexity: `O(V + E)` (linear scan + DFS).
22
23use crate::algorithms::paths::dijkstra::DijkstraMode;
24use crate::core::Graph;
25use crate::core::error::IgraphResult;
26use crate::core::graph::{EdgeId, VertexId};
27
28/// Returns `Some(root)` iff `graph` is a tree under `mode`,
29/// otherwise `None`. The null graph (`vcount == 0`) is **not** a
30/// tree by convention.
31///
32/// For undirected graphs the canonical root is `0`.
33/// For directed graphs:
34/// - [`DijkstraMode::Out`]: root is the unique vertex with
35///   in-degree `0`.
36/// - [`DijkstraMode::In`]: root is the unique vertex with
37///   out-degree `0`.
38/// - [`DijkstraMode::All`]: orientation ignored; canonical root
39///   is `0`.
40///
41/// Counterpart of `igraph_is_tree(_, _, _, mode)` from
42/// `references/igraph/src/properties/trees.c:251`.
43///
44/// # Examples
45///
46/// ```
47/// use rust_igraph::{Graph, is_tree, DijkstraMode};
48///
49/// // Undirected path 0-1-2-3: tree, root = 0.
50/// let mut g = Graph::with_vertices(4);
51/// g.add_edge(0, 1).unwrap();
52/// g.add_edge(1, 2).unwrap();
53/// g.add_edge(2, 3).unwrap();
54/// assert_eq!(is_tree(&g, DijkstraMode::All).unwrap(), Some(0));
55///
56/// // Out-arborescence rooted at 0: 0→1, 0→2, 1→3.
57/// let mut g = Graph::new(4, true).unwrap();
58/// g.add_edges(vec![(0u32, 1u32), (0, 2), (1, 3)]).unwrap();
59/// assert_eq!(is_tree(&g, DijkstraMode::Out).unwrap(), Some(0));
60///
61/// // 1→2←3→4 is an undirected forest, but not an out-tree.
62/// let mut g = Graph::new(4, true).unwrap();
63/// g.add_edges(vec![(0u32, 1u32), (2, 1), (2, 3)]).unwrap();
64/// assert_eq!(is_tree(&g, DijkstraMode::Out).unwrap(), None);
65/// // Treated as undirected, it has 3 edges on 4 vertices and is connected ⇒ tree.
66/// assert_eq!(is_tree(&g, DijkstraMode::All).unwrap(), Some(0));
67/// ```
68pub fn is_tree(graph: &Graph, mode: DijkstraMode) -> IgraphResult<Option<VertexId>> {
69    let n = graph.vcount();
70    let m = graph.ecount();
71
72    if n == 0 {
73        return Ok(None);
74    }
75
76    // A tree on N vertices has exactly N-1 edges.
77    if m as u64 != u64::from(n).saturating_sub(1) {
78        return Ok(None);
79    }
80
81    // Trivial: single vertex with no edges is a tree.
82    if n == 1 {
83        return Ok(Some(0));
84    }
85
86    let directed = graph.is_directed();
87    let effective_mode = if directed { mode } else { DijkstraMode::All };
88
89    let Some(root) = pick_root(graph, n, effective_mode)? else {
90        return Ok(None);
91    };
92
93    // DFS from root in `effective_mode` direction; count unique
94    // visited vertices. With ecount = vcount-1 and all vertices
95    // reached, the graph is necessarily acyclic and connected,
96    // hence a tree.
97    let visited_count = dfs_count(graph, n, root, effective_mode, directed)?;
98    if visited_count == n {
99        Ok(Some(root))
100    } else {
101        Ok(None)
102    }
103}
104
105/// Locate the canonical root vertex per `mode`. Returns `None` if
106/// the degree distribution disqualifies the graph from being a tree
107/// in this orientation.
108fn pick_root(graph: &Graph, n: VertexId, mode: DijkstraMode) -> IgraphResult<Option<VertexId>> {
109    match mode {
110        DijkstraMode::All => Ok(Some(0)),
111        DijkstraMode::Out => find_zero_degree_root(graph, n, true),
112        DijkstraMode::In => find_zero_degree_root(graph, n, false),
113    }
114}
115
116/// Linear scan for the first zero-degree (in-/out-) vertex; bail
117/// to `Ok(None)` on `degree > 1` (indicates a non-tree under this
118/// orientation).
119fn find_zero_degree_root(
120    graph: &Graph,
121    n: VertexId,
122    use_in_degree: bool,
123) -> IgraphResult<Option<VertexId>> {
124    for v in 0..n {
125        let d = if use_in_degree {
126            graph.incident_in(v)?.len()
127        } else {
128            graph.incident(v)?.len()
129        };
130        if d == 0 {
131            return Ok(Some(v));
132        }
133        if d > 1 {
134            return Ok(None);
135        }
136    }
137    Ok(None)
138}
139
140/// DFS from `root` in the orientation given by `mode` and count the
141/// number of distinct vertices visited.
142fn dfs_count(
143    graph: &Graph,
144    n: VertexId,
145    root: VertexId,
146    mode: DijkstraMode,
147    directed: bool,
148) -> IgraphResult<u32> {
149    let n_us = n as usize;
150    let mut visited: Vec<bool> = vec![false; n_us];
151    let mut stack: Vec<VertexId> = Vec::with_capacity(n_us);
152    let mut count: u32 = 0;
153
154    stack.push(root);
155    while let Some(u) = stack.pop() {
156        if visited[u as usize] {
157            continue;
158        }
159        visited[u as usize] = true;
160        count = count.saturating_add(1);
161
162        for eid in incident_edges(graph, u, mode, directed)? {
163            let nei = graph.edge_other(eid, u)?;
164            if !visited[nei as usize] {
165                stack.push(nei);
166            }
167        }
168    }
169    Ok(count)
170}
171
172/// Edges incident on `u` in the requested orientation.
173fn incident_edges(
174    graph: &Graph,
175    u: VertexId,
176    mode: DijkstraMode,
177    directed: bool,
178) -> IgraphResult<Vec<EdgeId>> {
179    match mode {
180        DijkstraMode::Out => graph.incident(u),
181        DijkstraMode::In => graph.incident_in(u),
182        DijkstraMode::All => {
183            if directed {
184                let mut out = graph.incident(u)?;
185                out.extend(graph.incident_in(u)?);
186                Ok(out)
187            } else {
188                graph.incident(u)
189            }
190        }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    // -------- Null / single-vertex --------
199
200    #[test]
201    fn null_graph_is_not_a_tree() {
202        let g = Graph::with_vertices(0);
203        assert_eq!(is_tree(&g, DijkstraMode::All).unwrap(), None);
204    }
205
206    #[test]
207    fn single_vertex_undirected_is_a_tree() {
208        let g = Graph::with_vertices(1);
209        assert_eq!(is_tree(&g, DijkstraMode::All).unwrap(), Some(0));
210    }
211
212    #[test]
213    fn single_vertex_directed_out_is_a_tree() {
214        let g = Graph::new(1, true).unwrap();
215        assert_eq!(is_tree(&g, DijkstraMode::Out).unwrap(), Some(0));
216    }
217
218    // -------- Undirected --------
219
220    #[test]
221    fn undirected_path_is_a_tree() {
222        let mut g = Graph::with_vertices(4);
223        g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3)]).unwrap();
224        assert_eq!(is_tree(&g, DijkstraMode::All).unwrap(), Some(0));
225    }
226
227    #[test]
228    fn undirected_star_is_a_tree() {
229        let mut g = Graph::with_vertices(5);
230        g.add_edges(vec![(0u32, 1u32), (0, 2), (0, 3), (0, 4)])
231            .unwrap();
232        assert_eq!(is_tree(&g, DijkstraMode::All).unwrap(), Some(0));
233    }
234
235    #[test]
236    fn undirected_triangle_is_not_a_tree() {
237        let mut g = Graph::with_vertices(3);
238        g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 0)]).unwrap();
239        assert_eq!(is_tree(&g, DijkstraMode::All).unwrap(), None);
240    }
241
242    #[test]
243    fn undirected_disconnected_is_not_a_tree() {
244        // Two disjoint edges: 0-1, 2-3. ecount=2, vcount=4 → fails ecount=vcount-1.
245        let mut g = Graph::with_vertices(4);
246        g.add_edges(vec![(0u32, 1u32), (2, 3)]).unwrap();
247        assert_eq!(is_tree(&g, DijkstraMode::All).unwrap(), None);
248    }
249
250    #[test]
251    fn undirected_disconnected_correct_edge_count_is_not_a_tree() {
252        // Two parallel + one isolated vertex: edges (0,1), (0,1), vcount=3.
253        // ecount=2 = vcount-1 BUT DFS only reaches {0,1}, not 2.
254        let mut g = Graph::with_vertices(3);
255        g.add_edge(0, 1).unwrap();
256        g.add_edge(0, 1).unwrap();
257        assert_eq!(is_tree(&g, DijkstraMode::All).unwrap(), None);
258    }
259
260    #[test]
261    fn undirected_self_loop_breaks_edge_count() {
262        // ecount=2 (self-loop + edge) = vcount-1=2 with vcount=3?
263        // vcount=3, ecount=2. edges (0,0), (1,2). ecount = vcount-1 PASSES.
264        // But DFS from 0 reaches only {0}, not {1,2}. → None.
265        let mut g = Graph::with_vertices(3);
266        g.add_edge(0, 0).unwrap();
267        g.add_edge(1, 2).unwrap();
268        assert_eq!(is_tree(&g, DijkstraMode::All).unwrap(), None);
269    }
270
271    // -------- Directed: OUT --------
272
273    #[test]
274    fn directed_out_arborescence_is_a_tree() {
275        // 0→1, 0→2, 1→3.
276        let mut g = Graph::new(4, true).unwrap();
277        g.add_edges(vec![(0u32, 1u32), (0, 2), (1, 3)]).unwrap();
278        assert_eq!(is_tree(&g, DijkstraMode::Out).unwrap(), Some(0));
279    }
280
281    #[test]
282    fn directed_out_chain_is_a_tree() {
283        // 0→1→2→3.
284        let mut g = Graph::new(4, true).unwrap();
285        g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3)]).unwrap();
286        assert_eq!(is_tree(&g, DijkstraMode::Out).unwrap(), Some(0));
287    }
288
289    #[test]
290    fn directed_in_arborescence_is_not_an_out_tree() {
291        // 1→0, 2→0, 3→1: every edge points TO root 0 — in-tree, not out-tree.
292        let mut g = Graph::new(4, true).unwrap();
293        g.add_edges(vec![(1u32, 0u32), (2, 0), (3, 1)]).unwrap();
294        assert_eq!(is_tree(&g, DijkstraMode::Out).unwrap(), None);
295        assert_eq!(is_tree(&g, DijkstraMode::In).unwrap(), Some(0));
296    }
297
298    #[test]
299    fn directed_v_pattern_to_center_is_not_an_out_tree() {
300        // 0→2, 1→2: vertex 2 has in-degree 2.
301        let mut g = Graph::new(3, true).unwrap();
302        g.add_edges(vec![(0u32, 2u32), (1, 2)]).unwrap();
303        assert_eq!(is_tree(&g, DijkstraMode::Out).unwrap(), None);
304    }
305
306    #[test]
307    fn directed_cycle_is_not_a_tree() {
308        // 0→1→2→0: 3 edges on 3 vertices, ecount ≠ vcount-1.
309        let mut g = Graph::new(3, true).unwrap();
310        g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 0)]).unwrap();
311        assert_eq!(is_tree(&g, DijkstraMode::Out).unwrap(), None);
312        assert_eq!(is_tree(&g, DijkstraMode::All).unwrap(), None);
313    }
314
315    #[test]
316    fn directed_all_mode_treats_as_undirected() {
317        // 1→2←3→4: not an out-tree, but undirected it's a path.
318        let mut g = Graph::new(4, true).unwrap();
319        g.add_edges(vec![(0u32, 1u32), (2, 1), (2, 3)]).unwrap();
320        assert_eq!(is_tree(&g, DijkstraMode::Out).unwrap(), None);
321        assert_eq!(is_tree(&g, DijkstraMode::All).unwrap(), Some(0));
322    }
323}