graph_advanced/
tarjan_scc.rs

1use graph_core::{Graph, NodeId};
2use std::collections::HashMap;
3
4/// Finds all **Strongly Connected Components** (SCCs) using Tarjan's algorithm.
5///
6/// A strongly connected component is a maximal set of nodes where every node
7/// is reachable from every other node via directed edges.
8///
9/// # Algorithm
10///
11/// A single DFS pass assigns each node a **discovery time** (`disc`) and a
12/// **low-link value** (`low[u]`): the smallest discovery time reachable from
13/// the subtree rooted at `u` via tree edges and at most one back-edge.
14///
15/// Nodes are pushed onto a stack as they are discovered. When we finish
16/// processing node `u` and find `low[u] == disc[u]`, `u` is the root of an
17/// SCC: we pop the stack until we reach `u`, and all popped nodes form one SCC.
18///
19/// # Returns
20///
21/// A `Vec` of SCCs, each SCC being a `Vec<NodeId>`. Components are returned
22/// in **reverse topological order** of the condensed DAG: if there is an edge
23/// from SCC A to SCC B, then B appears before A in the result.
24///
25/// # Complexity
26///
27/// O(V + E) — a single DFS pass over all nodes and edges.
28///
29/// # Examples
30///
31/// ```
32/// use graph_core::{AdjacencyList, Graph};
33/// use graph_advanced::tarjan_scc;
34///
35/// // Two-node cycle: {0, 1} form one SCC; {2} is its own.
36/// //   0 → 1 → 0
37/// //   1 → 2
38/// let mut g: AdjacencyList<()> = AdjacencyList::directed();
39/// let n: Vec<_> = (0..3).map(|_| g.add_node(())).collect();
40/// g.add_edge(n[0], n[1], 1.0).unwrap();
41/// g.add_edge(n[1], n[0], 1.0).unwrap();
42/// g.add_edge(n[1], n[2], 1.0).unwrap();
43///
44/// let sccs = tarjan_scc(&g);
45/// assert_eq!(sccs.len(), 2);
46/// ```
47pub fn tarjan_scc<G>(graph: &G) -> Vec<Vec<NodeId>>
48where
49    G: Graph<Weight = f64>,
50{
51    let mut state = TarjanState {
52        disc: HashMap::new(),
53        low: HashMap::new(),
54        on_stack: HashMap::new(),
55        stack: Vec::new(),
56        timer: 0,
57        result: Vec::new(),
58    };
59
60    for node in graph.nodes() {
61        if !state.disc.contains_key(&node) {
62            dfs_tarjan(graph, node, &mut state);
63        }
64    }
65
66    state.result
67}
68
69struct TarjanState {
70    disc: HashMap<NodeId, usize>,
71    low: HashMap<NodeId, usize>,
72    on_stack: HashMap<NodeId, bool>,
73    stack: Vec<NodeId>,
74    timer: usize,
75    result: Vec<Vec<NodeId>>,
76}
77
78fn dfs_tarjan<G>(graph: &G, node: NodeId, state: &mut TarjanState)
79where
80    G: Graph<Weight = f64>,
81{
82    state.disc.insert(node, state.timer);
83    state.low.insert(node, state.timer);
84    state.timer += 1;
85    state.on_stack.insert(node, true);
86    state.stack.push(node);
87
88    for (neighbour, _) in graph.neighbors(node) {
89        if !state.disc.contains_key(&neighbour) {
90            // Tree edge: recurse and pull up low-link.
91            dfs_tarjan(graph, neighbour, state);
92            let child_low = state.low[&neighbour];
93            let node_low = state.low.get_mut(&node).unwrap();
94            if child_low < *node_low {
95                *node_low = child_low;
96            }
97        } else if *state.on_stack.get(&neighbour).unwrap_or(&false) {
98            // Back edge to a node still on the stack (in current SCC).
99            let neighbour_disc = state.disc[&neighbour];
100            let node_low = state.low.get_mut(&node).unwrap();
101            if neighbour_disc < *node_low {
102                *node_low = neighbour_disc;
103            }
104        }
105    }
106
107    // If node is the root of an SCC, pop the stack to collect the component.
108    if state.low[&node] == state.disc[&node] {
109        let mut scc = Vec::new();
110        loop {
111            let w = state
112                .stack
113                .pop()
114                .expect("stack must not be empty during SCC pop");
115            state.on_stack.insert(w, false);
116            scc.push(w);
117            if w == node {
118                break;
119            }
120        }
121        state.result.push(scc);
122    }
123}