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: simplest variant [`dfs`], returns pre-order visit list.
6//! - ALGO-TR-003: multi-output variant [`dfs_tree`], returns parents,
7//!   discovery/finish timestamps, and pre/post-order.
8//! - [`dfs_simple`]: mode-aware DFS with direction control.
9
10use std::collections::VecDeque;
11
12use crate::core::{Graph, IgraphResult, VertexId};
13
14/// Result of a multi-output DFS scan from a single root. Returned by
15/// [`dfs_tree`].
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct DfsTree {
18    /// Vertices reachable from the root, in DFS pre-order (discovery order).
19    pub order: Vec<VertexId>,
20    /// Vertices in DFS post-order (finish order).
21    pub order_out: Vec<VertexId>,
22    /// `parents[v] == Some(p)` if `v` was DFS-discovered from `p`; `None`
23    /// for the root and for unreachable vertices.
24    pub parents: Vec<Option<VertexId>>,
25    /// `dist[v] == Some(d)` if `v` is reachable from the root at DFS-tree
26    /// depth `d` (0 for root); `None` if unreachable.
27    pub dist: Vec<Option<u32>>,
28}
29
30/// Multi-output DFS from `root`. Returns visit order (pre and post),
31/// per-vertex parents, and DFS-tree depth in a single pass.
32///
33/// Counterpart of `igraph_dfs(_, root, _, _, &order, &order_out, &father, &dist, _, _)`.
34///
35/// For directed graphs, traversal follows out-edges. Errors if `root`
36/// is out of range.
37///
38/// # Examples
39///
40/// ```
41/// use rust_igraph::{Graph, dfs_tree};
42///
43/// // Tree:
44/// //     0
45/// //    / \
46/// //   1   2
47/// //   |
48/// //   3
49/// let mut g = Graph::with_vertices(4);
50/// g.add_edge(0, 1).unwrap();
51/// g.add_edge(0, 2).unwrap();
52/// g.add_edge(1, 3).unwrap();
53///
54/// let r = dfs_tree(&g, 0).unwrap();
55/// assert_eq!(r.order[0], 0);
56/// assert_eq!(r.parents[0], None); // root
57/// assert_eq!(r.parents[1], Some(0));
58/// assert_eq!(r.parents[3], Some(1));
59/// assert_eq!(r.dist[3], Some(2));
60/// assert_eq!(r.order_out.last(), Some(&0)); // root finishes last
61/// ```
62pub fn dfs_tree(graph: &Graph, root: VertexId) -> IgraphResult<DfsTree> {
63    graph.neighbors_iter(root)?;
64
65    let n = graph.vcount();
66    let n_us = n as usize;
67    let mut visited = vec![false; n_us];
68    let mut order: Vec<VertexId> = Vec::with_capacity(n_us);
69    let mut order_out: Vec<VertexId> = Vec::with_capacity(n_us);
70    let mut parents: Vec<Option<VertexId>> = vec![None; n_us];
71    let mut dist: Vec<Option<u32>> = vec![None; n_us];
72
73    let mut stack: VecDeque<(VertexId, usize, Vec<VertexId>)> = VecDeque::new();
74
75    visited[root as usize] = true;
76    order.push(root);
77    dist[root as usize] = Some(0);
78    let mut root_neis: Vec<VertexId> = graph.neighbors_iter(root)?.collect();
79    root_neis.reverse();
80    stack.push_back((root, 0, root_neis));
81
82    while let Some(&(cur, cursor, ref neis)) = stack.back() {
83        let mut next_cursor = cursor;
84        let mut found: Option<VertexId> = None;
85        while next_cursor < neis.len() {
86            let nei = neis[next_cursor];
87            next_cursor += 1;
88            if !visited[nei as usize] {
89                found = Some(nei);
90                break;
91            }
92        }
93
94        if let Some(nei) = found {
95            let last = stack.len() - 1;
96            stack[last].1 = next_cursor;
97            visited[nei as usize] = true;
98            order.push(nei);
99            parents[nei as usize] = Some(cur);
100            #[allow(clippy::cast_possible_truncation)]
101            let depth = (stack.len()) as u32; // stack.len() == depth of new node
102            dist[nei as usize] = Some(depth);
103            let mut nei_neis: Vec<VertexId> = graph.neighbors_iter(nei)?.collect();
104            nei_neis.reverse();
105            stack.push_back((nei, 0, nei_neis));
106        } else {
107            order_out.push(cur);
108            stack.pop_back();
109        }
110    }
111
112    Ok(DfsTree {
113        order,
114        order_out,
115        parents,
116        dist,
117    })
118}
119
120/// Pre-order visit of vertices reachable from `root`, in DFS order.
121///
122/// Vertices unreachable from `root` are not included. Errors if `root`
123/// is not a valid vertex. Neighbour iteration follows
124/// [`Graph::neighbors`]'s order, which matches `python-igraph`'s
125/// `Graph.dfs` and the upstream `igraph_dfs` order.
126///
127/// # Examples
128///
129/// ```
130/// use rust_igraph::{Graph, dfs};
131///
132/// // 0 - 1 - 3
133/// // |
134/// // 2
135/// let mut g = Graph::with_vertices(4);
136/// g.add_edge(0, 1).unwrap();
137/// g.add_edge(0, 2).unwrap();
138/// g.add_edge(1, 3).unwrap();
139///
140/// let order = dfs(&g, 0).unwrap();
141/// assert_eq!(order[0], 0);                // always start at root
142/// assert_eq!(order.len(), 4);              // visits whole component
143/// ```
144pub fn dfs(graph: &Graph, root: VertexId) -> IgraphResult<Vec<VertexId>> {
145    // Validate root via the neighbour lookup; surfaces VertexOutOfRange.
146    graph.neighbors_iter(root)?;
147
148    let n = graph.vcount();
149    let mut visited = vec![false; n as usize];
150    let mut order: Vec<VertexId> = Vec::with_capacity(n as usize);
151
152    // Stack mirrors `igraph_stack_int_t`. Each entry stores the vertex
153    // and the cursor into its neighbour list — direct port of the C
154    // `nptr` array (visitors.c:516, :593).
155    let mut stack: VecDeque<(VertexId, usize, Vec<VertexId>)> = VecDeque::new();
156
157    visited[root as usize] = true;
158    order.push(root);
159    // Reverse the neighbour list before iteration: matches the upstream
160    // `igraph_dfs` pre-order on undirected graphs (and python-igraph's
161    // `Graph.dfs`). Concretely, igraph C's lazy adjacency list yields
162    // neighbours in opposite order to `igraph_neighbors` for DFS;
163    // pre-reversing here keeps our DFS in lockstep with both.
164    let mut root_neis: Vec<VertexId> = graph.neighbors_iter(root)?.collect();
165    root_neis.reverse();
166    stack.push_back((root, 0, root_neis));
167
168    while let Some(&(actvect, cursor, ref neis)) = stack.back() {
169        // Advance the cursor until we find an unvisited neighbour or
170        // exhaust the neighbour list.
171        let mut next_cursor = cursor;
172        let mut found: Option<VertexId> = None;
173        while next_cursor < neis.len() {
174            let nei = neis[next_cursor];
175            next_cursor += 1;
176            if !visited[nei as usize] {
177                found = Some(nei);
178                break;
179            }
180        }
181
182        if let Some(nei) = found {
183            // Update current frame's cursor; push the new vertex.
184            let last = stack.len() - 1;
185            stack[last].1 = next_cursor;
186            visited[nei as usize] = true;
187            order.push(nei);
188            let mut nei_neis: Vec<VertexId> = graph.neighbors_iter(nei)?.collect();
189            nei_neis.reverse();
190            stack.push_back((nei, 0, nei_neis));
191        } else {
192            // Subtree at `actvect` is complete; pop.
193            let _ = actvect;
194            stack.pop_back();
195        }
196    }
197
198    Ok(order)
199}
200
201/// Direction mode for [`dfs_simple`].
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub enum DfsMode {
204    /// Follow outgoing edges (default for directed graphs).
205    Out,
206    /// Follow incoming edges.
207    In,
208    /// Ignore edge direction (always used for undirected graphs).
209    All,
210}
211
212/// Result of [`dfs_simple`].
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct DfsSimple {
215    /// Vertices visited during the traversal, in DFS pre-order.
216    pub order: Vec<VertexId>,
217    /// Vertices in DFS post-order (finish order).
218    pub order_out: Vec<VertexId>,
219    /// `parents[v] == Some(p)` if vertex `v` was discovered via `p`;
220    /// `None` for root and unreachable vertices.
221    pub parents: Vec<Option<VertexId>>,
222    /// `dist[v] == Some(d)` if `v` is reachable at DFS-tree depth `d`;
223    /// `None` if unreachable.
224    pub dist: Vec<Option<u32>>,
225}
226
227/// Mode-aware DFS from a single root, returning pre/post-order,
228/// parents, and DFS-tree depth.
229///
230/// Counterpart of `igraph_dfs` from
231/// `references/igraph/src/graph/visitors.c`.
232///
233/// For undirected graphs the `mode` parameter is ignored. For directed
234/// graphs, [`DfsMode::Out`] follows outgoing edges, [`DfsMode::In`]
235/// follows incoming edges, and [`DfsMode::All`] ignores direction.
236///
237/// # Examples
238///
239/// ```
240/// use rust_igraph::{Graph, DfsMode, dfs_simple};
241///
242/// let mut g = Graph::new(4, true).unwrap();
243/// g.add_edge(0, 1).unwrap();
244/// g.add_edge(1, 2).unwrap();
245/// g.add_edge(0, 3).unwrap();
246///
247/// let r = dfs_simple(&g, 0, DfsMode::Out).unwrap();
248/// assert_eq!(r.order[0], 0);
249/// assert_eq!(r.parents[1], Some(0));
250/// assert_eq!(r.dist[2], Some(2));
251/// ```
252pub fn dfs_simple(graph: &Graph, root: VertexId, mode: DfsMode) -> IgraphResult<DfsSimple> {
253    graph.neighbors_iter(root)?; // validate root
254
255    let n = graph.vcount();
256    let n_us = n as usize;
257    let use_mode = if graph.is_directed() {
258        mode
259    } else {
260        DfsMode::All
261    };
262
263    let mut visited = vec![false; n_us];
264    let mut order: Vec<VertexId> = Vec::with_capacity(n_us);
265    let mut order_out: Vec<VertexId> = Vec::with_capacity(n_us);
266    let mut parents: Vec<Option<VertexId>> = vec![None; n_us];
267    let mut dist: Vec<Option<u32>> = vec![None; n_us];
268
269    let neighbors = |v: VertexId| -> IgraphResult<Vec<VertexId>> {
270        match use_mode {
271            DfsMode::Out => graph.out_neighbors_vec(v),
272            DfsMode::In => graph.in_neighbors_vec(v),
273            DfsMode::All => {
274                if graph.is_directed() {
275                    let mut combined = graph.out_neighbors_vec(v)?;
276                    combined.extend(graph.in_neighbors_vec(v)?);
277                    Ok(combined)
278                } else {
279                    Ok(graph.neighbors_iter(v)?.collect())
280                }
281            }
282        }
283    };
284
285    let mut stack: VecDeque<(VertexId, usize, Vec<VertexId>)> = VecDeque::new();
286
287    visited[root as usize] = true;
288    order.push(root);
289    dist[root as usize] = Some(0);
290    let mut root_neis = neighbors(root)?;
291    root_neis.reverse();
292    stack.push_back((root, 0, root_neis));
293
294    while let Some(&(cur, cursor, ref neis)) = stack.back() {
295        let mut next_cursor = cursor;
296        let mut found: Option<VertexId> = None;
297        while next_cursor < neis.len() {
298            let nei = neis[next_cursor];
299            next_cursor += 1;
300            if !visited[nei as usize] {
301                found = Some(nei);
302                break;
303            }
304        }
305
306        if let Some(nei) = found {
307            let last = stack.len() - 1;
308            stack[last].1 = next_cursor;
309            visited[nei as usize] = true;
310            order.push(nei);
311            parents[nei as usize] = Some(cur);
312            #[allow(clippy::cast_possible_truncation)]
313            let depth = stack.len() as u32;
314            dist[nei as usize] = Some(depth);
315            let mut nei_neis = neighbors(nei)?;
316            nei_neis.reverse();
317            stack.push_back((nei, 0, nei_neis));
318        } else {
319            order_out.push(cur);
320            stack.pop_back();
321        }
322    }
323
324    Ok(DfsSimple {
325        order,
326        order_out,
327        parents,
328        dist,
329    })
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    fn path_graph(n: u32) -> Graph {
337        let mut g = Graph::with_vertices(n);
338        for i in 0..n.saturating_sub(1) {
339            g.add_edge(i, i + 1).unwrap();
340        }
341        g
342    }
343
344    #[test]
345    fn empty_graph_errors_on_any_root() {
346        let g = Graph::with_vertices(0);
347        let err = dfs(&g, 0).unwrap_err();
348        assert!(matches!(
349            err,
350            crate::core::IgraphError::VertexOutOfRange { id: 0, n: 0 }
351        ));
352    }
353
354    #[test]
355    fn single_vertex_visits_self() {
356        let g = Graph::with_vertices(1);
357        assert_eq!(dfs(&g, 0).unwrap(), vec![0]);
358    }
359
360    #[test]
361    fn path_visits_in_order() {
362        let g = path_graph(5);
363        // Path 0-1-2-3-4: DFS from 0 follows the chain.
364        assert_eq!(dfs(&g, 0).unwrap(), vec![0, 1, 2, 3, 4]);
365    }
366
367    #[test]
368    fn dfs_goes_deep_before_wide() {
369        // 0 -- 1 -- 3
370        // |
371        // 2
372        // DFS from 0 must visit either 1-then-3 before backing up to 2,
373        // OR 2-then-back to 1-3. Critical: DFS visits one full subtree
374        // before another sibling (unlike BFS).
375        let mut g = Graph::with_vertices(4);
376        g.add_edge(0, 1).unwrap();
377        g.add_edge(0, 2).unwrap();
378        g.add_edge(1, 3).unwrap();
379        let order = dfs(&g, 0).unwrap();
380        assert_eq!(order[0], 0);
381        assert_eq!(order.len(), 4);
382        let pos = |v: u32| order.iter().position(|&x| x == v).unwrap();
383        // 3 is at depth 2 via 1; the DFS depth-first nature means
384        // pos(3) - pos(1) == 1 OR pos(3) > pos(1) without 2 in between.
385        // Easier invariant: 3 must be adjacent in `order` to either 1
386        // (parent) or part of the same subtree.
387        let p1 = pos(1);
388        let p3 = pos(3);
389        assert!(p3 == p1 + 1, "3 should follow 1 directly in DFS pre-order");
390    }
391
392    #[test]
393    fn unreachable_vertex_excluded() {
394        let mut g = Graph::with_vertices(3);
395        g.add_edge(0, 1).unwrap();
396        // Vertex 2 is isolated.
397        let order = dfs(&g, 0).unwrap();
398        assert_eq!(order.len(), 2);
399        assert!(order.contains(&0));
400        assert!(order.contains(&1));
401    }
402
403    #[test]
404    fn invalid_root_errors() {
405        let g = Graph::with_vertices(2);
406        let err = dfs(&g, 5).unwrap_err();
407        match err {
408            crate::core::IgraphError::VertexOutOfRange { id, n } => {
409                assert_eq!(id, 5);
410                assert_eq!(n, 2);
411            }
412            other => panic!("unexpected error: {other:?}"),
413        }
414    }
415
416    #[test]
417    fn complete_k4_visits_all_four() {
418        let mut g = Graph::with_vertices(4);
419        for u in 0..4u32 {
420            for v in (u + 1)..4 {
421                g.add_edge(u, v).unwrap();
422            }
423        }
424        let order = dfs(&g, 0).unwrap();
425        let mut sorted = order.clone();
426        sorted.sort_unstable();
427        assert_eq!(sorted, vec![0, 1, 2, 3]);
428        assert_eq!(order[0], 0);
429    }
430
431    // --- dfs_tree tests ---
432
433    #[test]
434    fn dfs_tree_singleton() {
435        let g = Graph::with_vertices(1);
436        let r = dfs_tree(&g, 0).unwrap();
437        assert_eq!(r.order, vec![0]);
438        assert_eq!(r.order_out, vec![0]);
439        assert_eq!(r.parents, vec![None]);
440        assert_eq!(r.dist, vec![Some(0)]);
441    }
442
443    #[test]
444    fn dfs_tree_path() {
445        let g = path_graph(4);
446        let r = dfs_tree(&g, 0).unwrap();
447        assert_eq!(r.order, vec![0, 1, 2, 3]);
448        assert_eq!(r.order_out, vec![3, 2, 1, 0]);
449        assert_eq!(r.parents, vec![None, Some(0), Some(1), Some(2)]);
450        assert_eq!(r.dist, vec![Some(0), Some(1), Some(2), Some(3)]);
451    }
452
453    #[test]
454    fn dfs_tree_branching() {
455        // 0-1, 0-2, 1-3
456        let mut g = Graph::with_vertices(4);
457        g.add_edge(0, 1).unwrap();
458        g.add_edge(0, 2).unwrap();
459        g.add_edge(1, 3).unwrap();
460        let r = dfs_tree(&g, 0).unwrap();
461        assert_eq!(r.order[0], 0);
462        assert_eq!(r.parents[0], None);
463        assert_eq!(r.dist[0], Some(0));
464        // Vertex 3's parent must be 1
465        assert_eq!(r.parents[3], Some(1));
466        assert_eq!(r.dist[3], Some(2));
467        // Root finishes last in post-order
468        assert_eq!(*r.order_out.last().unwrap(), 0);
469    }
470
471    #[test]
472    fn dfs_tree_unreachable() {
473        let mut g = Graph::with_vertices(3);
474        g.add_edge(0, 1).unwrap();
475        let r = dfs_tree(&g, 0).unwrap();
476        assert_eq!(r.order.len(), 2);
477        assert_eq!(r.order_out.len(), 2);
478        assert_eq!(r.parents[2], None);
479        assert_eq!(r.dist[2], None);
480    }
481
482    #[test]
483    fn dfs_tree_order_out_reverse_invariant() {
484        // For a tree (no cross-edges within reached component),
485        // order_out is the reverse of a valid topological post-ordering.
486        // Check that every parent finishes after all its children.
487        let mut g = Graph::with_vertices(5);
488        g.add_edge(0, 1).unwrap();
489        g.add_edge(0, 2).unwrap();
490        g.add_edge(1, 3).unwrap();
491        g.add_edge(1, 4).unwrap();
492        let r = dfs_tree(&g, 0).unwrap();
493        let finish_pos = |v: u32| r.order_out.iter().position(|&x| x == v).unwrap();
494        // Parent 0 finishes after children 1, 2
495        assert!(finish_pos(0) > finish_pos(1));
496        assert!(finish_pos(0) > finish_pos(2));
497        // Parent 1 finishes after children 3, 4
498        assert!(finish_pos(1) > finish_pos(3));
499        assert!(finish_pos(1) > finish_pos(4));
500    }
501
502    #[test]
503    fn dfs_tree_invalid_root() {
504        let g = Graph::with_vertices(2);
505        assert!(dfs_tree(&g, 5).is_err());
506    }
507
508    // ── dfs_simple tests ────────────────────────────────────────────
509
510    #[test]
511    fn dfs_simple_undirected_tree() {
512        let mut g = Graph::with_vertices(4);
513        g.add_edge(0, 1).unwrap();
514        g.add_edge(0, 2).unwrap();
515        g.add_edge(1, 3).unwrap();
516
517        let r = dfs_simple(&g, 0, DfsMode::Out).unwrap();
518        assert_eq!(r.order[0], 0);
519        assert_eq!(r.order.len(), 4);
520        assert_eq!(r.parents[0], None);
521        assert_eq!(r.dist[0], Some(0));
522        assert_eq!(*r.order_out.last().unwrap(), 0);
523    }
524
525    #[test]
526    fn dfs_simple_single_vertex() {
527        let g = Graph::with_vertices(1);
528        let r = dfs_simple(&g, 0, DfsMode::All).unwrap();
529        assert_eq!(r.order, vec![0]);
530        assert_eq!(r.order_out, vec![0]);
531        assert_eq!(r.dist, vec![Some(0)]);
532    }
533
534    #[test]
535    fn dfs_simple_invalid_root() {
536        let g = Graph::with_vertices(2);
537        assert!(dfs_simple(&g, 5, DfsMode::Out).is_err());
538    }
539
540    #[test]
541    fn dfs_simple_directed_out() {
542        // 0 -> 1 -> 2, 0 -> 3
543        let mut g = Graph::new(4, true).unwrap();
544        g.add_edge(0, 1).unwrap();
545        g.add_edge(1, 2).unwrap();
546        g.add_edge(0, 3).unwrap();
547
548        let r = dfs_simple(&g, 0, DfsMode::Out).unwrap();
549        assert_eq!(r.order[0], 0);
550        assert_eq!(r.order.len(), 4);
551        assert_eq!(r.parents[1], Some(0));
552        assert_eq!(r.parents[2], Some(1));
553        assert_eq!(r.dist[2], Some(2));
554    }
555
556    #[test]
557    fn dfs_simple_directed_in() {
558        // 1 -> 0, 2 -> 1, 3 -> 0
559        let mut g = Graph::new(4, true).unwrap();
560        g.add_edge(1, 0).unwrap();
561        g.add_edge(2, 1).unwrap();
562        g.add_edge(3, 0).unwrap();
563
564        let r = dfs_simple(&g, 0, DfsMode::In).unwrap();
565        assert_eq!(r.order[0], 0);
566        assert_eq!(r.order.len(), 4);
567        assert_eq!(r.parents[1], Some(0));
568        assert_eq!(r.parents[2], Some(1));
569        assert_eq!(r.parents[3], Some(0));
570    }
571
572    #[test]
573    fn dfs_simple_directed_all() {
574        // 0 -> 1, 2 -> 0
575        let mut g = Graph::new(3, true).unwrap();
576        g.add_edge(0, 1).unwrap();
577        g.add_edge(2, 0).unwrap();
578
579        let r = dfs_simple(&g, 0, DfsMode::All).unwrap();
580        assert_eq!(r.order.len(), 3);
581        assert_eq!(r.order[0], 0);
582    }
583
584    #[test]
585    fn dfs_simple_unreachable() {
586        let mut g = Graph::with_vertices(4);
587        g.add_edge(0, 1).unwrap();
588        let r = dfs_simple(&g, 0, DfsMode::All).unwrap();
589        assert_eq!(r.order.len(), 2);
590        assert_eq!(r.order_out.len(), 2);
591        assert_eq!(r.parents[2], None);
592        assert_eq!(r.dist[2], None);
593    }
594
595    #[test]
596    fn dfs_simple_mode_ignored_for_undirected() {
597        let mut g = Graph::with_vertices(3);
598        g.add_edge(0, 1).unwrap();
599        g.add_edge(1, 2).unwrap();
600        let r = dfs_simple(&g, 0, DfsMode::In).unwrap();
601        assert_eq!(r.order.len(), 3);
602    }
603
604    #[test]
605    fn dfs_simple_directed_out_no_reach() {
606        // 0 -> 1, 2 -> 0: from 0 following Out, only 0 and 1 reachable
607        let mut g = Graph::new(3, true).unwrap();
608        g.add_edge(0, 1).unwrap();
609        g.add_edge(2, 0).unwrap();
610        let r = dfs_simple(&g, 0, DfsMode::Out).unwrap();
611        assert_eq!(r.order.len(), 2);
612        assert!(r.order.contains(&0));
613        assert!(r.order.contains(&1));
614        assert_eq!(r.dist[2], None);
615    }
616}