Skip to main content

rust_igraph/algorithms/connectivity/
strong.rs

1//! Strongly connected components (ALGO-CC-002).
2//!
3//! Counterpart of `igraph_connected_components(_, _, _, _, IGRAPH_STRONG)` from
4//! `references/igraph/src/connectivity/components.c:203-386`.
5//!
6//! Algorithm: iterative two-pass DFS (Kosaraju), matching upstream igraph
7//! line-for-line so that membership labels agree with `python-igraph`'s
8//! `Graph.connected_components(mode='strong')` exactly (verified by
9//! `tests/oracle.rs::strongly_connected_components_*`).
10//!
11//! Pass 1 — forward DFS over **out**-neighbours from every unvisited vertex.
12//! When all out-edges of a vertex are exhausted, push it to a post-order
13//! stack `out`.
14//!
15//! Pass 2 — pop `out` from the back. Each unmarked grandfather seeds a new
16//! component; from there, do an iterative LIFO walk over **in**-neighbours
17//! to gather every vertex in the same SCC.
18//!
19//! Undirected graphs delegate to weak [`connected_components`] — the strong
20//! components of an undirected graph are the same as its weak components.
21//!
22//! Result shape is identical to weak components ([`ConnectedComponents`]):
23//! `membership` length `vcount`, dense ids `0..count` assigned in
24//! grandfather-pop order.
25//!
26//! Self-loops and parallel edges are handled identically to upstream
27//! (`IGRAPH_LOOPS_ONCE, IGRAPH_MULTIPLE` adjlist flags) — the
28//! `next_nei`/`visited` flag prevents re-pushing.
29
30use crate::algorithms::connectivity::components::{ConnectedComponents, connected_components};
31use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
32
33/// Compute the strongly connected components of `graph`.
34///
35/// Counterpart of `igraph_connected_components(_, _, _, _, IGRAPH_STRONG)`.
36/// On an undirected graph, returns the same partition as
37/// [`connected_components`].
38///
39/// # Examples
40///
41/// ```
42/// use rust_igraph::{Graph, strongly_connected_components};
43///
44/// // Two disjoint 3-cycles in a directed graph: 0→1→2→0 and 3→4→5→3.
45/// let mut g = Graph::new(6, true).unwrap();
46/// g.add_edge(0, 1).unwrap();
47/// g.add_edge(1, 2).unwrap();
48/// g.add_edge(2, 0).unwrap();
49/// g.add_edge(3, 4).unwrap();
50/// g.add_edge(4, 5).unwrap();
51/// g.add_edge(5, 3).unwrap();
52///
53/// let scc = strongly_connected_components(&g).unwrap();
54/// assert_eq!(scc.count, 2);
55/// assert_eq!(scc.membership[0], scc.membership[1]);
56/// assert_eq!(scc.membership[1], scc.membership[2]);
57/// assert_eq!(scc.membership[3], scc.membership[4]);
58/// assert_ne!(scc.membership[0], scc.membership[3]);
59/// ```
60pub fn strongly_connected_components(graph: &Graph) -> IgraphResult<ConnectedComponents> {
61    let n = graph.vcount();
62    if n == 0 {
63        return Ok(ConnectedComponents {
64            membership: Vec::new(),
65            count: 0,
66        });
67    }
68
69    if !graph.is_directed() {
70        return connected_components(graph);
71    }
72
73    let n_us = n as usize;
74
75    // Cache out-neighbour lists once. Same memory cost as upstream's
76    // `igraph_adjlist_init(graph, &adjlist, IGRAPH_OUT, ...)`.
77    let mut out_adj: Vec<Vec<VertexId>> = Vec::with_capacity(n_us);
78    for v in 0..n {
79        out_adj.push(graph.out_neighbors_vec(v)?);
80    }
81
82    // Pass 1: iterative forward DFS, post-order stack.
83    //
84    // `next_nei[v]` is upstream's eponymous counter:
85    //   0           — vertex never pushed onto the DFS stack;
86    //   1..=len     — DFS frame is live; next out-neighbour to consider
87    //                 is `out_adj[v][next_nei[v] - 1]`;
88    //   len + 1     — vertex has been popped (post-order recorded).
89    let mut next_nei: Vec<usize> = vec![0; n_us];
90    let mut out: Vec<VertexId> = Vec::with_capacity(n_us);
91    let mut stack: Vec<VertexId> = Vec::new();
92
93    for start in 0..n {
94        if next_nei[start as usize] > out_adj[start as usize].len() {
95            continue;
96        }
97        stack.push(start);
98        while let Some(&act) = stack.last() {
99            let act_us = act as usize;
100            let nbrs_len = out_adj[act_us].len();
101            if next_nei[act_us] == 0 {
102                // first visit
103                next_nei[act_us] = 1;
104            } else if next_nei[act_us] <= nbrs_len {
105                let nb = out_adj[act_us][next_nei[act_us] - 1];
106                if next_nei[nb as usize] == 0 {
107                    stack.push(nb);
108                }
109                next_nei[act_us] += 1;
110            } else {
111                // all out-edges processed; record post-order
112                out.push(act);
113                stack.pop();
114            }
115        }
116    }
117
118    // Cache in-neighbour lists for pass 2.
119    let mut in_adj: Vec<Vec<VertexId>> = Vec::with_capacity(n_us);
120    for v in 0..n {
121        in_adj.push(graph.in_neighbors_vec(v)?);
122    }
123
124    // Pass 2: pop `out` from the back; each unmarked grandfather seeds a
125    // new SCC. Visited vertices are flagged by `visited[v] = 1`.
126    let mut visited: Vec<u8> = vec![0; n_us];
127    let mut membership = vec![u32::MAX; n_us];
128    let mut count: u32 = 0;
129    let mut walk: Vec<VertexId> = Vec::new();
130
131    while let Some(grandfather) = out.pop() {
132        if visited[grandfather as usize] != 0 {
133            continue;
134        }
135        visited[grandfather as usize] = 1;
136        let comp_id = count;
137        count = count
138            .checked_add(1)
139            .ok_or(IgraphError::Internal("too many components"))?;
140        membership[grandfather as usize] = comp_id;
141
142        walk.clear();
143        walk.push(grandfather);
144        while let Some(v) = walk.pop() {
145            for &nb in &in_adj[v as usize] {
146                if visited[nb as usize] == 0 {
147                    visited[nb as usize] = 1;
148                    membership[nb as usize] = comp_id;
149                    walk.push(nb);
150                }
151            }
152        }
153    }
154
155    Ok(ConnectedComponents { membership, count })
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn empty_graph() {
164        let g = Graph::new(0, true).unwrap();
165        let scc = strongly_connected_components(&g).unwrap();
166        assert_eq!(scc.count, 0);
167        assert!(scc.membership.is_empty());
168    }
169
170    #[test]
171    fn single_vertex() {
172        let g = Graph::new(1, true).unwrap();
173        let scc = strongly_connected_components(&g).unwrap();
174        assert_eq!(scc.count, 1);
175        assert_eq!(scc.membership, vec![0]);
176    }
177
178    #[test]
179    fn n_isolated_vertices_yields_n_components() {
180        let g = Graph::new(5, true).unwrap();
181        let scc = strongly_connected_components(&g).unwrap();
182        assert_eq!(scc.count, 5);
183        // Order in which Kosaraju pops grandfathers from `out`. With no
184        // edges, pass-1 push/pop is `0,1,2,3,4` so `out = [0,1,2,3,4]`
185        // and pop order is `4,3,2,1,0`, giving labels `[4,3,2,1,0]`.
186        assert_eq!(scc.membership, vec![4, 3, 2, 1, 0]);
187    }
188
189    #[test]
190    fn directed_path_each_vertex_alone() {
191        // 0 -> 1 -> 2 -> 3: every vertex its own SCC.
192        let mut g = Graph::new(4, true).unwrap();
193        g.add_edge(0, 1).unwrap();
194        g.add_edge(1, 2).unwrap();
195        g.add_edge(2, 3).unwrap();
196        let scc = strongly_connected_components(&g).unwrap();
197        assert_eq!(scc.count, 4);
198        // Distinct labels for every vertex.
199        let mut sorted = scc.membership.clone();
200        sorted.sort_unstable();
201        sorted.dedup();
202        assert_eq!(sorted, vec![0, 1, 2, 3]);
203    }
204
205    #[test]
206    fn directed_3_cycle_is_one_component() {
207        // 0 -> 1 -> 2 -> 0
208        let mut g = Graph::new(3, true).unwrap();
209        g.add_edge(0, 1).unwrap();
210        g.add_edge(1, 2).unwrap();
211        g.add_edge(2, 0).unwrap();
212        let scc = strongly_connected_components(&g).unwrap();
213        assert_eq!(scc.count, 1);
214        assert_eq!(scc.membership, vec![0, 0, 0]);
215    }
216
217    #[test]
218    fn cycle_with_outgoing_chain() {
219        // 0 -> 1 -> 2 -> 0; 2 -> 3; 3 -> 4
220        // SCCs: {0,1,2}, {3}, {4}.
221        let mut g = Graph::new(5, true).unwrap();
222        g.add_edge(0, 1).unwrap();
223        g.add_edge(1, 2).unwrap();
224        g.add_edge(2, 0).unwrap();
225        g.add_edge(2, 3).unwrap();
226        g.add_edge(3, 4).unwrap();
227        let scc = strongly_connected_components(&g).unwrap();
228        assert_eq!(scc.count, 3);
229        // Labels must agree on the cycle.
230        assert_eq!(scc.membership[0], scc.membership[1]);
231        assert_eq!(scc.membership[1], scc.membership[2]);
232        // Tail vertices are singletons distinct from each other and from the cycle.
233        assert_ne!(scc.membership[3], scc.membership[0]);
234        assert_ne!(scc.membership[4], scc.membership[0]);
235        assert_ne!(scc.membership[3], scc.membership[4]);
236    }
237
238    #[test]
239    fn two_disjoint_cycles() {
240        // 0 -> 1 -> 2 -> 0; 3 -> 4 -> 5 -> 3
241        let mut g = Graph::new(6, true).unwrap();
242        g.add_edge(0, 1).unwrap();
243        g.add_edge(1, 2).unwrap();
244        g.add_edge(2, 0).unwrap();
245        g.add_edge(3, 4).unwrap();
246        g.add_edge(4, 5).unwrap();
247        g.add_edge(5, 3).unwrap();
248        let scc = strongly_connected_components(&g).unwrap();
249        assert_eq!(scc.count, 2);
250        assert_eq!(scc.membership[0], scc.membership[1]);
251        assert_eq!(scc.membership[1], scc.membership[2]);
252        assert_eq!(scc.membership[3], scc.membership[4]);
253        assert_eq!(scc.membership[4], scc.membership[5]);
254        assert_ne!(scc.membership[0], scc.membership[3]);
255    }
256
257    #[test]
258    fn self_loop_does_not_split_component() {
259        // 0 -> 0; 0 -> 1; 1 -> 0 → SCC {0,1}.
260        let mut g = Graph::new(2, true).unwrap();
261        g.add_edge(0, 0).unwrap();
262        g.add_edge(0, 1).unwrap();
263        g.add_edge(1, 0).unwrap();
264        let scc = strongly_connected_components(&g).unwrap();
265        assert_eq!(scc.count, 1);
266        assert_eq!(scc.membership, vec![0, 0]);
267    }
268
269    #[test]
270    fn parallel_edges_do_not_split_component() {
271        // Three parallel edges 0 -> 1 plus a return 1 -> 0 → SCC {0,1}.
272        let mut g = Graph::new(2, true).unwrap();
273        g.add_edge(0, 1).unwrap();
274        g.add_edge(0, 1).unwrap();
275        g.add_edge(0, 1).unwrap();
276        g.add_edge(1, 0).unwrap();
277        let scc = strongly_connected_components(&g).unwrap();
278        assert_eq!(scc.count, 1);
279        assert_eq!(scc.membership, vec![0, 0]);
280    }
281
282    #[test]
283    fn undirected_graph_delegates_to_weak() {
284        // Undirected 5-vertex with two components → SCC == WCC.
285        let mut g = Graph::with_vertices(5);
286        g.add_edge(0, 1).unwrap();
287        g.add_edge(1, 2).unwrap();
288        g.add_edge(3, 4).unwrap();
289        let scc = strongly_connected_components(&g).unwrap();
290        let wcc = connected_components(&g).unwrap();
291        assert_eq!(scc.membership, wcc.membership);
292        assert_eq!(scc.count, wcc.count);
293    }
294
295    #[test]
296    fn membership_ids_are_dense() {
297        // 0 -> 1 -> 2 -> 0; 3 -> 4 (chain) → 3 components, ids 0..3.
298        let mut g = Graph::new(5, true).unwrap();
299        g.add_edge(0, 1).unwrap();
300        g.add_edge(1, 2).unwrap();
301        g.add_edge(2, 0).unwrap();
302        g.add_edge(3, 4).unwrap();
303        let scc = strongly_connected_components(&g).unwrap();
304        assert_eq!(scc.count, 3);
305        let mut seen: Vec<u32> = scc.membership.clone();
306        seen.sort_unstable();
307        seen.dedup();
308        assert_eq!(seen, vec![0, 1, 2]);
309    }
310}