Skip to main content

trueno_graph/algorithms/
structure.rs

1//! Graph structure algorithms: connected components and strongly connected components
2//!
3//! Provides structural analysis algorithms:
4//! - `connected_components`: Count weakly connected components
5//! - `kosaraju_scc`: Find strongly connected components using Kosaraju's algorithm
6//!
7//! # Example
8//!
9//! ```
10//! use trueno_graph::{CsrGraph, NodeId, connected_components, kosaraju_scc};
11//!
12//! // Build a graph with two components: 0 → 1, 2 → 3
13//! let edges = vec![
14//!     (NodeId(0), NodeId(1), 1.0),
15//!     (NodeId(2), NodeId(3), 1.0),
16//! ];
17//! let graph = CsrGraph::from_edge_list(&edges).unwrap();
18//!
19//! // Two weakly connected components
20//! assert_eq!(connected_components(&graph), 2);
21//!
22//! // Four SCCs (each node is its own SCC in a DAG)
23//! let sccs = kosaraju_scc(&graph);
24//! assert_eq!(sccs.len(), 4);
25//! ```
26
27use crate::storage::CsrGraph;
28use crate::NodeId;
29
30/// Count the number of weakly connected components in the graph
31///
32/// Treats the graph as undirected for connectivity purposes.
33/// Two nodes are in the same component if there's a path between them
34/// (ignoring edge direction).
35///
36/// # Arguments
37///
38/// * `graph` - The CSR graph to analyze
39///
40/// # Returns
41///
42/// The number of weakly connected components
43///
44/// # Example
45///
46/// ```
47/// use trueno_graph::{CsrGraph, NodeId, connected_components};
48///
49/// // Two disconnected edges: 0 → 1, 2 → 3
50/// let edges = vec![
51///     (NodeId(0), NodeId(1), 1.0),
52///     (NodeId(2), NodeId(3), 1.0),
53/// ];
54/// let graph = CsrGraph::from_edge_list(&edges).unwrap();
55///
56/// assert_eq!(connected_components(&graph), 2);
57/// ```
58#[must_use]
59pub fn connected_components(graph: &CsrGraph) -> usize {
60    let n = graph.num_nodes();
61    if n == 0 {
62        return 0;
63    }
64
65    let mut visited = vec![false; n];
66    let mut count = 0;
67
68    for start in 0..n {
69        if !visited[start] {
70            // BFS/DFS to mark all reachable nodes (treating as undirected)
71            undirected_dfs(graph, start, &mut visited);
72            count += 1;
73        }
74    }
75
76    count
77}
78
79/// DFS treating graph as undirected (follows both incoming and outgoing edges)
80fn undirected_dfs(graph: &CsrGraph, node: usize, visited: &mut [bool]) {
81    if visited[node] {
82        return;
83    }
84    visited[node] = true;
85
86    #[allow(clippy::cast_possible_truncation)]
87    let node_id = NodeId(node as u32);
88
89    // Follow outgoing edges
90    if let Ok(neighbors) = graph.outgoing_neighbors(node_id) {
91        for &neighbor in neighbors {
92            let neighbor_idx = neighbor as usize;
93            if !visited[neighbor_idx] {
94                undirected_dfs(graph, neighbor_idx, visited);
95            }
96        }
97    }
98
99    // Follow incoming edges (treat as undirected)
100    if let Ok(neighbors) = graph.incoming_neighbors(node_id) {
101        for &neighbor in neighbors {
102            let neighbor_idx = neighbor as usize;
103            if !visited[neighbor_idx] {
104                undirected_dfs(graph, neighbor_idx, visited);
105            }
106        }
107    }
108}
109
110/// Find strongly connected components using Kosaraju's algorithm
111///
112/// A strongly connected component (SCC) is a maximal set of nodes where
113/// every node is reachable from every other node following edge directions.
114///
115/// # Arguments
116///
117/// * `graph` - The CSR graph to analyze
118///
119/// # Returns
120///
121/// A vector of SCCs, where each SCC is a vector of `NodeId`s.
122/// SCCs are returned in reverse topological order (sink SCCs first).
123///
124/// # Algorithm
125///
126/// Kosaraju's algorithm runs in O(V + E):
127/// 1. DFS on original graph to get finish order
128/// 2. DFS on transpose graph in reverse finish order
129/// 3. Each DFS tree in step 2 is an SCC
130///
131/// # Example
132///
133/// ```
134/// use trueno_graph::{CsrGraph, NodeId, kosaraju_scc};
135///
136/// // Cycle: 0 → 1 → 2 → 0
137/// let edges = vec![
138///     (NodeId(0), NodeId(1), 1.0),
139///     (NodeId(1), NodeId(2), 1.0),
140///     (NodeId(2), NodeId(0), 1.0),
141/// ];
142/// let graph = CsrGraph::from_edge_list(&edges).unwrap();
143///
144/// let sccs = kosaraju_scc(&graph);
145/// // All three nodes form one SCC
146/// assert_eq!(sccs.len(), 1);
147/// assert_eq!(sccs[0].len(), 3);
148/// ```
149#[must_use]
150pub fn kosaraju_scc(graph: &CsrGraph) -> Vec<Vec<NodeId>> {
151    let n = graph.num_nodes();
152    if n == 0 {
153        return Vec::new();
154    }
155
156    // Step 1: DFS to get finish order
157    let mut visited = vec![false; n];
158    let mut finish_order = Vec::with_capacity(n);
159
160    for start in 0..n {
161        if !visited[start] {
162            dfs_finish_order(graph, start, &mut visited, &mut finish_order);
163        }
164    }
165
166    // Step 2: DFS on transpose in reverse finish order
167    let mut visited = vec![false; n];
168    let mut sccs = Vec::new();
169
170    for &node in finish_order.iter().rev() {
171        if !visited[node] {
172            let mut component = Vec::new();
173            dfs_transpose(graph, node, &mut visited, &mut component);
174            sccs.push(component);
175        }
176    }
177
178    sccs
179}
180
181/// DFS to compute finish order (post-order)
182fn dfs_finish_order(
183    graph: &CsrGraph,
184    node: usize,
185    visited: &mut [bool],
186    finish_order: &mut Vec<usize>,
187) {
188    visited[node] = true;
189
190    #[allow(clippy::cast_possible_truncation)]
191    if let Ok(neighbors) = graph.outgoing_neighbors(NodeId(node as u32)) {
192        for &neighbor in neighbors {
193            let neighbor_idx = neighbor as usize;
194            if !visited[neighbor_idx] {
195                dfs_finish_order(graph, neighbor_idx, visited, finish_order);
196            }
197        }
198    }
199
200    finish_order.push(node);
201}
202
203/// DFS on transpose graph (follow incoming edges instead of outgoing)
204fn dfs_transpose(graph: &CsrGraph, node: usize, visited: &mut [bool], component: &mut Vec<NodeId>) {
205    visited[node] = true;
206
207    #[allow(clippy::cast_possible_truncation)]
208    component.push(NodeId(node as u32));
209
210    // Follow incoming edges (transpose of outgoing)
211    #[allow(clippy::cast_possible_truncation)]
212    if let Ok(neighbors) = graph.incoming_neighbors(NodeId(node as u32)) {
213        for &neighbor in neighbors {
214            let neighbor_idx = neighbor as usize;
215            if !visited[neighbor_idx] {
216                dfs_transpose(graph, neighbor_idx, visited, component);
217            }
218        }
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn test_empty_graph_components() {
228        let graph = CsrGraph::new();
229        assert_eq!(connected_components(&graph), 0);
230    }
231
232    #[test]
233    fn test_empty_graph_scc() {
234        let graph = CsrGraph::new();
235        let sccs = kosaraju_scc(&graph);
236        assert!(sccs.is_empty());
237    }
238
239    #[test]
240    fn test_single_node_component() {
241        let edges = vec![(NodeId(0), NodeId(1), 1.0)];
242        let graph = CsrGraph::from_edge_list(&edges).unwrap();
243        // 0 and 1 are connected
244        assert_eq!(connected_components(&graph), 1);
245    }
246
247    #[test]
248    fn test_two_disconnected_edges() {
249        let edges = vec![(NodeId(0), NodeId(1), 1.0), (NodeId(2), NodeId(3), 1.0)];
250        let graph = CsrGraph::from_edge_list(&edges).unwrap();
251        assert_eq!(connected_components(&graph), 2);
252    }
253
254    #[test]
255    fn test_chain_single_component() {
256        // 0 → 1 → 2 → 3
257        let edges = vec![
258            (NodeId(0), NodeId(1), 1.0),
259            (NodeId(1), NodeId(2), 1.0),
260            (NodeId(2), NodeId(3), 1.0),
261        ];
262        let graph = CsrGraph::from_edge_list(&edges).unwrap();
263        assert_eq!(connected_components(&graph), 1);
264    }
265
266    #[test]
267    fn test_diamond_single_component() {
268        // Diamond: 0 → 1 → 3, 0 → 2 → 3
269        let edges = vec![
270            (NodeId(0), NodeId(1), 1.0),
271            (NodeId(0), NodeId(2), 1.0),
272            (NodeId(1), NodeId(3), 1.0),
273            (NodeId(2), NodeId(3), 1.0),
274        ];
275        let graph = CsrGraph::from_edge_list(&edges).unwrap();
276        assert_eq!(connected_components(&graph), 1);
277    }
278
279    #[test]
280    fn test_scc_dag_each_node_separate() {
281        // DAG: 0 → 1 → 2 (no cycles, each node is its own SCC)
282        let edges = vec![(NodeId(0), NodeId(1), 1.0), (NodeId(1), NodeId(2), 1.0)];
283        let graph = CsrGraph::from_edge_list(&edges).unwrap();
284        let sccs = kosaraju_scc(&graph);
285        assert_eq!(sccs.len(), 3);
286    }
287
288    #[test]
289    fn test_scc_simple_cycle() {
290        // Cycle: 0 → 1 → 2 → 0
291        let edges = vec![
292            (NodeId(0), NodeId(1), 1.0),
293            (NodeId(1), NodeId(2), 1.0),
294            (NodeId(2), NodeId(0), 1.0),
295        ];
296        let graph = CsrGraph::from_edge_list(&edges).unwrap();
297        let sccs = kosaraju_scc(&graph);
298        // All three form one SCC
299        assert_eq!(sccs.len(), 1);
300        assert_eq!(sccs[0].len(), 3);
301    }
302
303    #[test]
304    fn test_scc_two_node_cycle() {
305        // 0 ↔ 1
306        let edges = vec![(NodeId(0), NodeId(1), 1.0), (NodeId(1), NodeId(0), 1.0)];
307        let graph = CsrGraph::from_edge_list(&edges).unwrap();
308        let sccs = kosaraju_scc(&graph);
309        assert_eq!(sccs.len(), 1);
310        assert_eq!(sccs[0].len(), 2);
311    }
312
313    #[test]
314    fn test_scc_self_loop() {
315        // Self-loop: 0 → 0
316        let edges = vec![(NodeId(0), NodeId(0), 1.0)];
317        let graph = CsrGraph::from_edge_list(&edges).unwrap();
318        let sccs = kosaraju_scc(&graph);
319        assert_eq!(sccs.len(), 1);
320        assert_eq!(sccs[0].len(), 1);
321    }
322
323    #[test]
324    fn test_scc_two_separate_cycles() {
325        // Two separate cycles: 0 ↔ 1, 2 ↔ 3
326        let edges = vec![
327            (NodeId(0), NodeId(1), 1.0),
328            (NodeId(1), NodeId(0), 1.0),
329            (NodeId(2), NodeId(3), 1.0),
330            (NodeId(3), NodeId(2), 1.0),
331        ];
332        let graph = CsrGraph::from_edge_list(&edges).unwrap();
333        let sccs = kosaraju_scc(&graph);
334        assert_eq!(sccs.len(), 2);
335        assert!(sccs.iter().all(|scc| scc.len() == 2));
336    }
337
338    #[test]
339    fn test_scc_complex_graph() {
340        // Complex: Two SCCs connected by a bridge
341        // SCC1: 0 ↔ 1, SCC2: 2 ↔ 3, Bridge: 1 → 2
342        let edges = vec![
343            (NodeId(0), NodeId(1), 1.0),
344            (NodeId(1), NodeId(0), 1.0),
345            (NodeId(1), NodeId(2), 1.0), // Bridge
346            (NodeId(2), NodeId(3), 1.0),
347            (NodeId(3), NodeId(2), 1.0),
348        ];
349        let graph = CsrGraph::from_edge_list(&edges).unwrap();
350        let sccs = kosaraju_scc(&graph);
351        assert_eq!(sccs.len(), 2);
352    }
353
354    #[test]
355    fn test_scc_disconnected_with_cycles() {
356        // Two disconnected cycles
357        let edges = vec![
358            (NodeId(0), NodeId(1), 1.0),
359            (NodeId(1), NodeId(2), 1.0),
360            (NodeId(2), NodeId(0), 1.0),
361            (NodeId(3), NodeId(4), 1.0),
362            (NodeId(4), NodeId(3), 1.0),
363        ];
364        let graph = CsrGraph::from_edge_list(&edges).unwrap();
365        let sccs = kosaraju_scc(&graph);
366        assert_eq!(sccs.len(), 2);
367        // One SCC has 3 nodes, one has 2
368        let sizes: Vec<_> = sccs.iter().map(std::vec::Vec::len).collect();
369        assert!(sizes.contains(&3));
370        assert!(sizes.contains(&2));
371    }
372
373    #[test]
374    fn test_connected_components_with_cycle() {
375        // Cycle forms one component
376        let edges = vec![
377            (NodeId(0), NodeId(1), 1.0),
378            (NodeId(1), NodeId(2), 1.0),
379            (NodeId(2), NodeId(0), 1.0),
380        ];
381        let graph = CsrGraph::from_edge_list(&edges).unwrap();
382        assert_eq!(connected_components(&graph), 1);
383    }
384
385    #[test]
386    fn test_three_separate_components() {
387        let edges = vec![
388            (NodeId(0), NodeId(1), 1.0),
389            (NodeId(2), NodeId(3), 1.0),
390            (NodeId(4), NodeId(5), 1.0),
391        ];
392        let graph = CsrGraph::from_edge_list(&edges).unwrap();
393        assert_eq!(connected_components(&graph), 3);
394    }
395}