Skip to main content

rust_igraph/algorithms/traversal/
dfs.rs

1//! Depth-first search.
2//!
3//! Counterpart of `igraph_dfs()` from
4//! `references/igraph/src/graph/visitors.c:479-661`.
5//! ALGO-TR-002 ships the simplest variant: from one root, return the
6//! pre-order visit list. Full callback-driven DFS (in/out callbacks,
7//! `parents` / `dist` / `order_out`, unreachable mode) lands in a
8//! follow-up AWU.
9
10use std::collections::VecDeque;
11
12use crate::core::{Graph, IgraphResult, VertexId};
13
14/// Pre-order visit of vertices reachable from `root`, in DFS order.
15///
16/// Vertices unreachable from `root` are not included. Errors if `root`
17/// is not a valid vertex. Neighbour iteration follows
18/// [`Graph::neighbors`]'s order, which matches `python-igraph`'s
19/// `Graph.dfs` and the upstream `igraph_dfs` order.
20///
21/// # Examples
22///
23/// ```
24/// use rust_igraph::{Graph, dfs};
25///
26/// // 0 - 1 - 3
27/// // |
28/// // 2
29/// let mut g = Graph::with_vertices(4);
30/// g.add_edge(0, 1).unwrap();
31/// g.add_edge(0, 2).unwrap();
32/// g.add_edge(1, 3).unwrap();
33///
34/// let order = dfs(&g, 0).unwrap();
35/// assert_eq!(order[0], 0);                // always start at root
36/// assert_eq!(order.len(), 4);              // visits whole component
37/// ```
38pub fn dfs(graph: &Graph, root: VertexId) -> IgraphResult<Vec<VertexId>> {
39    // Validate root via the neighbour lookup; surfaces VertexOutOfRange.
40    let _ = graph.neighbors(root)?;
41
42    let n = graph.vcount();
43    let mut visited = vec![false; n as usize];
44    let mut order: Vec<VertexId> = Vec::with_capacity(n as usize);
45
46    // Stack mirrors `igraph_stack_int_t`. Each entry stores the vertex
47    // and the cursor into its neighbour list — direct port of the C
48    // `nptr` array (visitors.c:516, :593).
49    let mut stack: VecDeque<(VertexId, usize, Vec<VertexId>)> = VecDeque::new();
50
51    visited[root as usize] = true;
52    order.push(root);
53    // Reverse the neighbour list before iteration: matches the upstream
54    // `igraph_dfs` pre-order on undirected graphs (and python-igraph's
55    // `Graph.dfs`). Concretely, igraph C's lazy adjacency list yields
56    // neighbours in opposite order to `igraph_neighbors` for DFS;
57    // pre-reversing here keeps our DFS in lockstep with both.
58    let mut root_neis = graph.neighbors(root)?;
59    root_neis.reverse();
60    stack.push_back((root, 0, root_neis));
61
62    while let Some(&(actvect, cursor, ref neis)) = stack.back() {
63        // Advance the cursor until we find an unvisited neighbour or
64        // exhaust the neighbour list.
65        let mut next_cursor = cursor;
66        let mut found: Option<VertexId> = None;
67        while next_cursor < neis.len() {
68            let nei = neis[next_cursor];
69            next_cursor += 1;
70            if !visited[nei as usize] {
71                found = Some(nei);
72                break;
73            }
74        }
75
76        if let Some(nei) = found {
77            // Update current frame's cursor; push the new vertex.
78            let last = stack.len() - 1;
79            stack[last].1 = next_cursor;
80            visited[nei as usize] = true;
81            order.push(nei);
82            let mut nei_neis = graph.neighbors(nei)?;
83            nei_neis.reverse();
84            stack.push_back((nei, 0, nei_neis));
85        } else {
86            // Subtree at `actvect` is complete; pop.
87            let _ = actvect;
88            stack.pop_back();
89        }
90    }
91
92    Ok(order)
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    fn path_graph(n: u32) -> Graph {
100        let mut g = Graph::with_vertices(n);
101        for i in 0..n.saturating_sub(1) {
102            g.add_edge(i, i + 1).unwrap();
103        }
104        g
105    }
106
107    #[test]
108    fn empty_graph_errors_on_any_root() {
109        let g = Graph::with_vertices(0);
110        let err = dfs(&g, 0).unwrap_err();
111        assert!(matches!(
112            err,
113            crate::core::IgraphError::VertexOutOfRange { id: 0, n: 0 }
114        ));
115    }
116
117    #[test]
118    fn single_vertex_visits_self() {
119        let g = Graph::with_vertices(1);
120        assert_eq!(dfs(&g, 0).unwrap(), vec![0]);
121    }
122
123    #[test]
124    fn path_visits_in_order() {
125        let g = path_graph(5);
126        // Path 0-1-2-3-4: DFS from 0 follows the chain.
127        assert_eq!(dfs(&g, 0).unwrap(), vec![0, 1, 2, 3, 4]);
128    }
129
130    #[test]
131    fn dfs_goes_deep_before_wide() {
132        // 0 -- 1 -- 3
133        // |
134        // 2
135        // DFS from 0 must visit either 1-then-3 before backing up to 2,
136        // OR 2-then-back to 1-3. Critical: DFS visits one full subtree
137        // before another sibling (unlike BFS).
138        let mut g = Graph::with_vertices(4);
139        g.add_edge(0, 1).unwrap();
140        g.add_edge(0, 2).unwrap();
141        g.add_edge(1, 3).unwrap();
142        let order = dfs(&g, 0).unwrap();
143        assert_eq!(order[0], 0);
144        assert_eq!(order.len(), 4);
145        let pos = |v: u32| order.iter().position(|&x| x == v).unwrap();
146        // 3 is at depth 2 via 1; the DFS depth-first nature means
147        // pos(3) - pos(1) == 1 OR pos(3) > pos(1) without 2 in between.
148        // Easier invariant: 3 must be adjacent in `order` to either 1
149        // (parent) or part of the same subtree.
150        let p1 = pos(1);
151        let p3 = pos(3);
152        assert!(p3 == p1 + 1, "3 should follow 1 directly in DFS pre-order");
153    }
154
155    #[test]
156    fn unreachable_vertex_excluded() {
157        let mut g = Graph::with_vertices(3);
158        g.add_edge(0, 1).unwrap();
159        // Vertex 2 is isolated.
160        let order = dfs(&g, 0).unwrap();
161        assert_eq!(order.len(), 2);
162        assert!(order.contains(&0));
163        assert!(order.contains(&1));
164    }
165
166    #[test]
167    fn invalid_root_errors() {
168        let g = Graph::with_vertices(2);
169        let err = dfs(&g, 5).unwrap_err();
170        match err {
171            crate::core::IgraphError::VertexOutOfRange { id, n } => {
172                assert_eq!(id, 5);
173                assert_eq!(n, 2);
174            }
175            other => panic!("unexpected error: {other:?}"),
176        }
177    }
178
179    #[test]
180    fn complete_k4_visits_all_four() {
181        let mut g = Graph::with_vertices(4);
182        for u in 0..4u32 {
183            for v in (u + 1)..4 {
184                g.add_edge(u, v).unwrap();
185            }
186        }
187        let order = dfs(&g, 0).unwrap();
188        let mut sorted = order.clone();
189        sorted.sort_unstable();
190        assert_eq!(sorted, vec![0, 1, 2, 3]);
191        assert_eq!(order[0], 0);
192    }
193}