Skip to main content

ringkernel_graph/algorithms/
scc.rs

1//! Strongly Connected Components (SCC) algorithms.
2//!
3//! A strongly connected component is a maximal subgraph where every node
4//! can reach every other node. This module provides:
5//!
6//! - Tarjan's algorithm (sequential, O(V+E))
7//! - Kosaraju's algorithm (can be parallelized)
8
9use crate::models::{ComponentId, CsrMatrix, NodeId};
10use crate::Result;
11
12/// SCC configuration.
13#[derive(Debug, Clone, Default)]
14pub struct SccConfig {
15    /// Return components in topological order.
16    pub topological_order: bool,
17}
18
19impl SccConfig {
20    /// Create new SCC configuration.
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    /// Return components in topological order.
26    pub fn with_topological_order(mut self) -> Self {
27        self.topological_order = true;
28        self
29    }
30}
31
32/// Tarjan's SCC algorithm (sequential).
33///
34/// Uses DFS with lowlink values to find SCCs in O(V+E) time.
35/// Returns component ID for each node.
36pub fn scc_tarjan(adj: &CsrMatrix) -> Result<Vec<ComponentId>> {
37    scc_tarjan_with_config(adj, &SccConfig::default())
38}
39
40/// Tarjan's algorithm with configuration.
41pub fn scc_tarjan_with_config(adj: &CsrMatrix, _config: &SccConfig) -> Result<Vec<ComponentId>> {
42    if adj.num_rows == 0 {
43        return Ok(Vec::new());
44    }
45
46    let n = adj.num_rows;
47    let mut component = vec![ComponentId::UNASSIGNED; n];
48    let mut index = vec![u32::MAX; n]; // Discovery index
49    let mut lowlink = vec![u32::MAX; n];
50    let mut on_stack = vec![false; n];
51    let mut stack = Vec::new();
52
53    let mut current_index = 0u32;
54    let mut current_component = 0u32;
55
56    // Need to handle non-recursive implementation to avoid stack overflow
57    for start in 0..n {
58        if index[start] != u32::MAX {
59            continue;
60        }
61
62        // Iterative DFS with explicit stack
63        let mut dfs_stack: Vec<(usize, usize)> = vec![(start, 0)];
64
65        while let Some(&(v, neighbor_idx)) = dfs_stack.last() {
66            if neighbor_idx == 0 {
67                // First visit to this node
68                index[v] = current_index;
69                lowlink[v] = current_index;
70                current_index += 1;
71                stack.push(v);
72                on_stack[v] = true;
73            }
74
75            let neighbors = adj.neighbors(NodeId(v as u32));
76
77            if neighbor_idx < neighbors.len() {
78                let w = neighbors[neighbor_idx] as usize;
79                dfs_stack.last_mut().unwrap().1 += 1;
80
81                if index[w] == u32::MAX {
82                    // Not visited, push to DFS stack
83                    dfs_stack.push((w, 0));
84                } else if on_stack[w] {
85                    // Back edge to node in current SCC
86                    lowlink[v] = lowlink[v].min(index[w]);
87                }
88            } else {
89                // All neighbors processed
90                dfs_stack.pop();
91
92                // Update parent's lowlink
93                if let Some(&(parent, _)) = dfs_stack.last() {
94                    lowlink[parent] = lowlink[parent].min(lowlink[v]);
95                }
96
97                // Check if v is root of an SCC
98                if lowlink[v] == index[v] {
99                    // Pop all nodes in this SCC
100                    loop {
101                        let w = stack.pop().unwrap();
102                        on_stack[w] = false;
103                        component[w] = ComponentId::new(current_component);
104                        if w == v {
105                            break;
106                        }
107                    }
108                    current_component += 1;
109                }
110            }
111        }
112    }
113
114    Ok(component)
115}
116
117/// Kosaraju's SCC algorithm.
118///
119/// Two-pass algorithm:
120/// 1. DFS on original graph, record finish order
121/// 2. DFS on transposed graph in reverse finish order
122///
123/// More parallelizable than Tarjan's.
124pub fn scc_kosaraju(adj: &CsrMatrix) -> Result<Vec<ComponentId>> {
125    scc_kosaraju_with_config(adj, &SccConfig::default())
126}
127
128/// Kosaraju's algorithm with configuration.
129pub fn scc_kosaraju_with_config(adj: &CsrMatrix, _config: &SccConfig) -> Result<Vec<ComponentId>> {
130    if adj.num_rows == 0 {
131        return Ok(Vec::new());
132    }
133
134    let n = adj.num_rows;
135
136    // Pass 1: DFS on original graph, record finish order
137    let mut visited = vec![false; n];
138    let mut finish_order = Vec::with_capacity(n);
139
140    for start in 0..n {
141        if !visited[start] {
142            dfs_finish_order(adj, start, &mut visited, &mut finish_order);
143        }
144    }
145
146    // Create transposed graph
147    let transposed = adj.transpose();
148
149    // Pass 2: DFS on transposed graph in reverse finish order
150    let mut component = vec![ComponentId::UNASSIGNED; n];
151    let mut current_component = 0u32;
152    visited.fill(false);
153
154    for &node in finish_order.iter().rev() {
155        if !visited[node] {
156            dfs_assign_component(
157                &transposed,
158                node,
159                &mut visited,
160                &mut component,
161                ComponentId::new(current_component),
162            );
163            current_component += 1;
164        }
165    }
166
167    Ok(component)
168}
169
170/// DFS to record finish order.
171fn dfs_finish_order(
172    adj: &CsrMatrix,
173    start: usize,
174    visited: &mut [bool],
175    finish_order: &mut Vec<usize>,
176) {
177    let mut stack = vec![(start, false)]; // (node, processed)
178
179    while let Some((node, processed)) = stack.pop() {
180        if processed {
181            finish_order.push(node);
182            continue;
183        }
184
185        if visited[node] {
186            continue;
187        }
188        visited[node] = true;
189
190        // Push this node again to record finish time after children
191        stack.push((node, true));
192
193        for &neighbor in adj.neighbors(NodeId(node as u32)) {
194            let w = neighbor as usize;
195            if !visited[w] {
196                stack.push((w, false));
197            }
198        }
199    }
200}
201
202/// DFS to assign component IDs.
203fn dfs_assign_component(
204    adj: &CsrMatrix,
205    start: usize,
206    visited: &mut [bool],
207    component: &mut [ComponentId],
208    comp_id: ComponentId,
209) {
210    let mut stack = vec![start];
211
212    while let Some(node) = stack.pop() {
213        if visited[node] {
214            continue;
215        }
216        visited[node] = true;
217        component[node] = comp_id;
218
219        for &neighbor in adj.neighbors(NodeId(node as u32)) {
220            let w = neighbor as usize;
221            if !visited[w] {
222                stack.push(w);
223            }
224        }
225    }
226}
227
228/// Get number of SCCs from component assignment.
229pub fn count_components(components: &[ComponentId]) -> usize {
230    if components.is_empty() {
231        return 0;
232    }
233    components
234        .iter()
235        .filter(|c| c.is_assigned())
236        .map(|c| c.0)
237        .max()
238        .map(|m| m as usize + 1)
239        .unwrap_or(0)
240}
241
242/// Get nodes in each component.
243pub fn get_component_members(components: &[ComponentId]) -> Vec<Vec<NodeId>> {
244    let num_components = count_components(components);
245    let mut members = vec![Vec::new(); num_components];
246
247    for (node_id, &comp_id) in components.iter().enumerate() {
248        if comp_id.is_assigned() {
249            members[comp_id.0 as usize].push(NodeId(node_id as u32));
250        }
251    }
252
253    members
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_single_node() {
262        let adj = CsrMatrix::empty(1);
263        let components = scc_tarjan(&adj).unwrap();
264        assert_eq!(components.len(), 1);
265        assert!(components[0].is_assigned());
266    }
267
268    #[test]
269    fn test_line_graph_sccs() {
270        // Line graph: 0 -> 1 -> 2 -> 3
271        // Each node is its own SCC (no cycles)
272        let edges = [(0, 1), (1, 2), (2, 3)];
273        let adj = CsrMatrix::from_edges(4, &edges);
274
275        let components = scc_tarjan(&adj).unwrap();
276        assert_eq!(count_components(&components), 4);
277    }
278
279    #[test]
280    fn test_cycle_scc() {
281        // Cycle: 0 -> 1 -> 2 -> 0
282        // All nodes in same SCC
283        let edges = [(0, 1), (1, 2), (2, 0)];
284        let adj = CsrMatrix::from_edges(3, &edges);
285
286        let components = scc_tarjan(&adj).unwrap();
287        assert_eq!(count_components(&components), 1);
288        assert_eq!(components[0], components[1]);
289        assert_eq!(components[1], components[2]);
290    }
291
292    #[test]
293    fn test_two_sccs() {
294        // Two cycles: 0 <-> 1, 2 <-> 3, with 1 -> 2
295        let edges = [(0, 1), (1, 0), (1, 2), (2, 3), (3, 2)];
296        let adj = CsrMatrix::from_edges(4, &edges);
297
298        let components = scc_tarjan(&adj).unwrap();
299        assert_eq!(count_components(&components), 2);
300
301        // 0 and 1 should be in same component
302        assert_eq!(components[0], components[1]);
303        // 2 and 3 should be in same component
304        assert_eq!(components[2], components[3]);
305        // Different components
306        assert_ne!(components[0], components[2]);
307    }
308
309    #[test]
310    fn test_kosaraju_same_as_tarjan() {
311        // More complex graph
312        let edges = [
313            (0, 1),
314            (1, 2),
315            (2, 0), // Cycle 0-1-2
316            (2, 3),
317            (3, 4),
318            (4, 3), // Cycle 3-4
319            (5, 6),
320            (6, 5), // Cycle 5-6
321        ];
322        let adj = CsrMatrix::from_edges(7, &edges);
323
324        let tarjan = scc_tarjan(&adj).unwrap();
325        let kosaraju = scc_kosaraju(&adj).unwrap();
326
327        // Same number of components
328        assert_eq!(count_components(&tarjan), count_components(&kosaraju));
329
330        // Same groupings (component IDs may differ)
331        let tarjan_members = get_component_members(&tarjan);
332        let kosaraju_members = get_component_members(&kosaraju);
333
334        assert_eq!(tarjan_members.len(), kosaraju_members.len());
335
336        // Sort members within each component for comparison
337        let mut tarjan_sorted: Vec<_> = tarjan_members
338            .iter()
339            .map(|v| {
340                let mut v = v.clone();
341                v.sort();
342                v
343            })
344            .collect();
345        tarjan_sorted.sort();
346
347        let mut kosaraju_sorted: Vec<_> = kosaraju_members
348            .iter()
349            .map(|v| {
350                let mut v = v.clone();
351                v.sort();
352                v
353            })
354            .collect();
355        kosaraju_sorted.sort();
356
357        assert_eq!(tarjan_sorted, kosaraju_sorted);
358    }
359
360    #[test]
361    fn test_disconnected_graph() {
362        // Two disconnected nodes
363        let adj = CsrMatrix::empty(2);
364
365        let components = scc_tarjan(&adj).unwrap();
366        assert_eq!(count_components(&components), 2);
367        assert_ne!(components[0], components[1]);
368    }
369
370    #[test]
371    fn test_get_component_members() {
372        let edges = [(0, 1), (1, 0), (2, 3), (3, 2)];
373        let adj = CsrMatrix::from_edges(4, &edges);
374
375        let components = scc_tarjan(&adj).unwrap();
376        let members = get_component_members(&components);
377
378        assert_eq!(members.len(), 2);
379
380        // Check that each component has 2 members
381        for comp in &members {
382            assert_eq!(comp.len(), 2);
383        }
384    }
385
386    #[test]
387    fn test_empty_graph() {
388        let adj = CsrMatrix::empty(0);
389        let components = scc_tarjan(&adj).unwrap();
390        assert!(components.is_empty());
391    }
392}