Skip to main content

rust_igraph/algorithms/connectivity/
biconnected.rs

1//! Biconnected components (ALGO-CC-011).
2//!
3//! Counterpart of `igraph_biconnected_components()` from
4//! `references/igraph/src/connectivity/components.c:1032-1227`. Same
5//! iterative DFS with low-link tracking that powers
6//! [`super::articulation::articulation_points`], extended to also
7//! collect the vertex set and tree edges of every biconnected
8//! component, plus the count.
9//!
10//! Phase-1 minimal slice ships these outputs in one bundle:
11//! - `count` — total number of biconnected components.
12//! - `components[i]` — vertex ids of the i-th component.
13//! - `tree_edges[i]` — edge ids that form a spanning tree of the i-th
14//!   component (i.e. the DFS-tree edges of that component, popped off
15//!   the edge stack).
16//! - `component_edges[i]` — *all* edge ids inside the i-th component
17//!   (not just the spanning tree). Counterpart of upstream's
18//!   `component_edges` argument; see ALGO-CC-012.
19//! - `articulation_points` — vertices whose removal would disconnect
20//!   the graph (deduplicated, in DFS-discovery order).
21//!
22//! igraph's convention: isolated vertices are *not* part of any
23//! biconnected component (see upstream doc at
24//! `components.c:990-998`). The two-vertex graph with one edge is
25//! biconnected.
26
27use crate::core::graph::EdgeId;
28use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
29
30/// Output of [`biconnected_components`].
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct BiconnectedComponents {
33    /// Total number of biconnected components.
34    pub count: u32,
35    /// `components[i]` lists the vertex ids of the i-th component.
36    pub components: Vec<Vec<VertexId>>,
37    /// `tree_edges[i]` lists the edge ids that form a spanning tree of
38    /// the i-th component. Same length as `components`.
39    pub tree_edges: Vec<Vec<EdgeId>>,
40    /// `component_edges[i]` lists *every* edge id whose endpoints both
41    /// lie in the i-th component. Same length as `components`.
42    /// Counterpart of upstream's `component_edges` output (CC-012).
43    pub component_edges: Vec<Vec<EdgeId>>,
44    /// Articulation points of the graph (in DFS-discovery order).
45    pub articulation_points: Vec<VertexId>,
46}
47
48/// Compute the biconnected components of `graph`.
49///
50/// Counterpart of `igraph_biconnected_components()`. The graph is
51/// treated as undirected. Isolated vertices are not part of any
52/// component. See [`BiconnectedComponents`] for the output shape.
53///
54/// # Examples
55///
56/// ```
57/// use rust_igraph::{Graph, biconnected_components};
58///
59/// // Cycle 0-1-2-0 plus pendant 2-3-4: components are {0,1,2}, {2,3}, {3,4}.
60/// let mut g = Graph::with_vertices(5);
61/// g.add_edge(0, 1).unwrap();
62/// g.add_edge(1, 2).unwrap();
63/// g.add_edge(2, 0).unwrap();
64/// g.add_edge(2, 3).unwrap();
65/// g.add_edge(3, 4).unwrap();
66/// let bc = biconnected_components(&g).unwrap();
67/// assert_eq!(bc.count, 3);
68/// let mut aps = bc.articulation_points.clone();
69/// aps.sort_unstable();
70/// assert_eq!(aps, vec![2, 3]);
71/// // component_edges partitions every edge across all components.
72/// let total: usize = bc.component_edges.iter().map(Vec::len).sum();
73/// assert_eq!(total, g.ecount());
74/// ```
75#[allow(clippy::too_many_lines)] // Direct port of upstream's tightly-coupled DFS state.
76pub fn biconnected_components(graph: &Graph) -> IgraphResult<BiconnectedComponents> {
77    let n = graph.vcount();
78    let mut out = BiconnectedComponents {
79        count: 0,
80        components: Vec::new(),
81        tree_edges: Vec::new(),
82        component_edges: Vec::new(),
83        articulation_points: Vec::new(),
84    };
85    if n == 0 {
86        return Ok(out);
87    }
88    let n_us = n as usize;
89
90    // Per-vertex DFS state, identical to ALGO-CC-010 articulation_points.
91    let mut num: Vec<u32> = vec![0; n_us];
92    let mut low: Vec<u32> = vec![0; n_us];
93    let mut nextptr: Vec<usize> = vec![0; n_us];
94    let mut found: Vec<bool> = vec![false; n_us];
95    let mut path: Vec<VertexId> = Vec::new();
96    // edgestack tracks edges in DFS-tree order; on AP detection we pop
97    // back to the parent's edge to extract the just-finished component.
98    let mut edgestack: Vec<EdgeId> = Vec::new();
99    let mut counter: u32 = 1;
100
101    // Pre-cache incident edges (matches upstream `IGRAPH_LOOPS_TWICE`).
102    let mut inc: Vec<Vec<EdgeId>> = Vec::with_capacity(n_us);
103    for v in 0..n {
104        inc.push(graph.incident(v)?);
105    }
106
107    // `comps_so_far` is upstream's `comps` counter — used to mark
108    // `vertex_added` so the same vertex isn't double-counted within a
109    // component's vertex list.
110    let mut comps_so_far: u32 = 0;
111    let mut vertex_added: Vec<u32> = vec![0; n_us]; // 0 = not added
112
113    for i in 0..n {
114        if low[i as usize] != 0 {
115            continue;
116        }
117        path.push(i);
118        let mut rootdfs: u32 = 0;
119        num[i as usize] = counter;
120        low[i as usize] = counter;
121        counter = counter
122            .checked_add(1)
123            .ok_or(IgraphError::Internal("DFS counter overflow"))?;
124
125        while let Some(&act) = path.last() {
126            let act_us = act as usize;
127            let nbrs = &inc[act_us];
128            let actnext = nextptr[act_us];
129
130            if actnext < nbrs.len() {
131                let edge = nbrs[actnext];
132                let nei = graph.edge_other(edge, act)?;
133                if low[nei as usize] == 0 {
134                    if act == i {
135                        rootdfs = rootdfs.saturating_add(1);
136                    }
137                    edgestack.push(edge);
138                    path.push(nei);
139                    num[nei as usize] = counter;
140                    low[nei as usize] = counter;
141                    counter = counter
142                        .checked_add(1)
143                        .ok_or(IgraphError::Internal("DFS counter overflow"))?;
144                } else if num[nei as usize] < low[act_us] {
145                    low[act_us] = num[nei as usize];
146                }
147                nextptr[act_us] += 1;
148            } else {
149                // Step UP.
150                path.pop();
151                if let Some(&prev) = path.last() {
152                    let prev_us = prev as usize;
153                    if low[act_us] < low[prev_us] {
154                        low[prev_us] = low[act_us];
155                    }
156                    if low[act_us] >= num[prev_us] {
157                        // Articulation point detection (skip the root).
158                        if !found[prev_us] && prev != i {
159                            out.articulation_points.push(prev);
160                            found[prev_us] = true;
161                        }
162                        out.count = out
163                            .count
164                            .checked_add(1)
165                            .ok_or(IgraphError::Internal("component count overflow"))?;
166                        comps_so_far = comps_so_far
167                            .checked_add(1)
168                            .ok_or(IgraphError::Internal("component count overflow"))?;
169
170                        // Pop edges off `edgestack` until we hit one
171                        // touching `prev` — those edges form the just-finished
172                        // biconnected component.
173                        let mut comp_tree_edges: Vec<EdgeId> = Vec::new();
174                        let mut comp_vertices: Vec<VertexId> = Vec::new();
175                        while let Some(e) = edgestack.pop() {
176                            let (from, to) = graph.edge(e)?;
177                            comp_tree_edges.push(e);
178                            if vertex_added[from as usize] != comps_so_far {
179                                vertex_added[from as usize] = comps_so_far;
180                                comp_vertices.push(from);
181                            }
182                            if vertex_added[to as usize] != comps_so_far {
183                                vertex_added[to as usize] = comps_so_far;
184                                comp_vertices.push(to);
185                            }
186                            if from == prev || to == prev {
187                                break;
188                            }
189                        }
190                        // After comp_vertices is built, re-scan each
191                        // vertex's incidence to gather every edge whose
192                        // other endpoint is also in this component. The
193                        // `nei < vert` guard prevents picking each edge
194                        // twice — direct port of upstream's
195                        // `component_edges` block at components.c:1216.
196                        let mut comp_edges: Vec<EdgeId> = Vec::new();
197                        for &vert in &comp_vertices {
198                            for &e in &inc[vert as usize] {
199                                let nei = graph.edge_other(e, vert)?;
200                                if vertex_added[nei as usize] == comps_so_far && nei < vert {
201                                    comp_edges.push(e);
202                                }
203                            }
204                        }
205                        out.components.push(comp_vertices);
206                        out.tree_edges.push(comp_tree_edges);
207                        out.component_edges.push(comp_edges);
208                    }
209                }
210            }
211        }
212
213        if rootdfs >= 2 {
214            out.articulation_points.push(i);
215        }
216    }
217
218    Ok(out)
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    fn sorted<T: Ord + Clone>(slice: &[T]) -> Vec<T> {
226        let mut v = slice.to_vec();
227        v.sort_unstable();
228        v
229    }
230
231    #[test]
232    fn empty_graph_yields_empty() {
233        let g = Graph::with_vertices(0);
234        let bc = biconnected_components(&g).unwrap();
235        assert_eq!(bc.count, 0);
236        assert!(bc.components.is_empty());
237        assert!(bc.tree_edges.is_empty());
238        assert!(bc.component_edges.is_empty());
239        assert!(bc.articulation_points.is_empty());
240    }
241
242    #[test]
243    fn isolated_vertices_yield_zero_components() {
244        let g = Graph::with_vertices(5);
245        let bc = biconnected_components(&g).unwrap();
246        assert_eq!(bc.count, 0);
247        assert!(bc.components.is_empty());
248        assert!(bc.component_edges.is_empty());
249    }
250
251    #[test]
252    fn two_vertices_one_edge_is_one_component() {
253        let mut g = Graph::with_vertices(2);
254        g.add_edge(0, 1).unwrap();
255        let bc = biconnected_components(&g).unwrap();
256        assert_eq!(bc.count, 1);
257        assert_eq!(sorted(&bc.components[0]), vec![0, 1]);
258        assert_eq!(bc.tree_edges[0], vec![0]);
259        assert_eq!(bc.component_edges[0], vec![0]);
260        assert!(bc.articulation_points.is_empty());
261    }
262
263    #[test]
264    fn triangle_is_single_biconnected_component() {
265        let mut g = Graph::with_vertices(3);
266        g.add_edge(0, 1).unwrap();
267        g.add_edge(1, 2).unwrap();
268        g.add_edge(2, 0).unwrap();
269        let bc = biconnected_components(&g).unwrap();
270        assert_eq!(bc.count, 1);
271        assert_eq!(sorted(&bc.components[0]), vec![0, 1, 2]);
272        // All 3 cycle edges must appear in component_edges (not just tree).
273        assert_eq!(sorted(&bc.component_edges[0]), vec![0, 1, 2]);
274        // Spanning tree of a 3-cycle has only 2 edges.
275        assert_eq!(bc.tree_edges[0].len(), 2);
276        assert!(bc.articulation_points.is_empty());
277    }
278
279    #[test]
280    fn k4_complete_component_edges_has_all_six_edges() {
281        let mut g = Graph::with_vertices(4);
282        for u in 0..4u32 {
283            for v in (u + 1)..4 {
284                g.add_edge(u, v).unwrap();
285            }
286        }
287        let bc = biconnected_components(&g).unwrap();
288        assert_eq!(bc.count, 1);
289        // K4: all 6 edges, but spanning tree only has 3.
290        assert_eq!(bc.component_edges[0].len(), 6);
291        assert_eq!(bc.tree_edges[0].len(), 3);
292        let mut all = bc.component_edges[0].clone();
293        all.sort_unstable();
294        assert_eq!(all, vec![0, 1, 2, 3, 4, 5]);
295    }
296
297    #[test]
298    fn pendant_component_edges_match_tree() {
299        // Bridge edge alone: tree_edges == component_edges (single edge).
300        // 0-1-2-0 cycle + 2-3 bridge. The 2-3 component has 1 edge total.
301        let mut g = Graph::with_vertices(4);
302        g.add_edge(0, 1).unwrap(); // edge 0
303        g.add_edge(1, 2).unwrap(); // edge 1
304        g.add_edge(2, 0).unwrap(); // edge 2
305        g.add_edge(2, 3).unwrap(); // edge 3
306        let bc = biconnected_components(&g).unwrap();
307        assert_eq!(bc.count, 2);
308        // Find the bridge component (size 2 vertices, 1 edge).
309        let bridge_idx = bc
310            .components
311            .iter()
312            .position(|c| c.len() == 2)
313            .expect("bridge component");
314        assert_eq!(bc.tree_edges[bridge_idx], bc.component_edges[bridge_idx]);
315        // Cycle component has 3 edges in component_edges, 2 in tree_edges.
316        let cycle_idx = 1 - bridge_idx;
317        assert_eq!(bc.component_edges[cycle_idx].len(), 3);
318        assert_eq!(bc.tree_edges[cycle_idx].len(), 2);
319    }
320
321    #[test]
322    fn component_edges_partition_non_bridge_edges() {
323        // Every non-bridge edge belongs to exactly one biconnected
324        // component; bridge edges are themselves single-edge components.
325        // Combined, component_edges across all components partition E.
326        let mut g = Graph::with_vertices(7);
327        for &(u, v) in &[
328            (0u32, 1),
329            (1, 2),
330            (2, 0),
331            (2, 3),
332            (3, 4),
333            (4, 5),
334            (5, 3),
335            (0, 6),
336        ] {
337            g.add_edge(u, v).unwrap();
338        }
339        let bc = biconnected_components(&g).unwrap();
340        let total: usize = bc.component_edges.iter().map(Vec::len).sum();
341        // 8 edges, all should be partitioned (no edge in two components).
342        assert_eq!(total, g.ecount());
343        // No duplicate edge ids across components.
344        let mut all: Vec<_> = bc.component_edges.iter().flatten().copied().collect();
345        all.sort_unstable();
346        let mut deduped = all.clone();
347        deduped.dedup();
348        assert_eq!(all, deduped);
349    }
350
351    #[test]
352    fn cycle_with_pendant_three_components() {
353        // 0-1-2-0 cycle + 2-3-4 pendant: components are {0,1,2}, {2,3}, {3,4}.
354        let mut g = Graph::with_vertices(5);
355        g.add_edge(0, 1).unwrap();
356        g.add_edge(1, 2).unwrap();
357        g.add_edge(2, 0).unwrap();
358        g.add_edge(2, 3).unwrap();
359        g.add_edge(3, 4).unwrap();
360        let bc = biconnected_components(&g).unwrap();
361        assert_eq!(bc.count, 3);
362        let mut aps = bc.articulation_points.clone();
363        aps.sort_unstable();
364        assert_eq!(aps, vec![2, 3]);
365        // Each component has the right size; sum equals 7
366        // (cycle contributes 3 vertices, each pendant edge contributes 2,
367        // shared vertex 2 counted twice, shared vertex 3 counted twice).
368        let total_vertices: usize = bc.components.iter().map(Vec::len).sum();
369        assert_eq!(total_vertices, 7);
370    }
371
372    #[test]
373    fn star_4_three_components() {
374        // 0-1, 0-2, 0-3: each edge is its own biconnected component.
375        let mut g = Graph::with_vertices(4);
376        for v in 1..4 {
377            g.add_edge(0, v).unwrap();
378        }
379        let bc = biconnected_components(&g).unwrap();
380        assert_eq!(bc.count, 3);
381        assert_eq!(bc.articulation_points, vec![0]);
382        for comp in &bc.components {
383            assert_eq!(comp.len(), 2);
384        }
385    }
386
387    #[test]
388    fn upstream_test_fixture_produces_4_components() {
389        // From references/igraph/tests/unit/igraph_biconnected_components.c
390        // 10v graph (vertex 9 isolated), 9 edges:
391        //   0-1, 1-2, 2-3, 3-0, 2-4, 4-5, 5-2, 5-6, 7-8.
392        // Expected per upstream .out: 4 components, articulation points {2, 5}.
393        let mut g = Graph::with_vertices(10);
394        for &(u, v) in &[
395            (0u32, 1),
396            (1, 2),
397            (2, 3),
398            (3, 0),
399            (2, 4),
400            (4, 5),
401            (5, 2),
402            (5, 6),
403            (7, 8),
404        ] {
405            g.add_edge(u, v).unwrap();
406        }
407        let bc = biconnected_components(&g).unwrap();
408        assert_eq!(bc.count, 4);
409        let mut aps = bc.articulation_points.clone();
410        aps.sort_unstable();
411        assert_eq!(aps, vec![2, 5]);
412    }
413
414    #[test]
415    fn k4_complete_one_component() {
416        let mut g = Graph::with_vertices(4);
417        for u in 0..4u32 {
418            for v in (u + 1)..4 {
419                g.add_edge(u, v).unwrap();
420            }
421        }
422        let bc = biconnected_components(&g).unwrap();
423        assert_eq!(bc.count, 1);
424        assert_eq!(sorted(&bc.components[0]), vec![0, 1, 2, 3]);
425        assert!(bc.articulation_points.is_empty());
426    }
427
428    #[test]
429    fn aps_match_articulation_points_function() {
430        // Cross-check: the AP set from this function matches CC-010.
431        let mut g = Graph::with_vertices(7);
432        for &(u, v) in &[
433            (0u32, 1),
434            (1, 2),
435            (2, 0),
436            (2, 3),
437            (3, 4),
438            (4, 5),
439            (5, 3),
440            (0, 6),
441        ] {
442            g.add_edge(u, v).unwrap();
443        }
444        let bc = biconnected_components(&g).unwrap();
445        let mut bc_aps = bc.articulation_points.clone();
446        bc_aps.sort_unstable();
447        let mut ap = super::super::articulation::articulation_points(&g).unwrap();
448        ap.sort_unstable();
449        assert_eq!(bc_aps, ap);
450    }
451}