Skip to main content

rust_igraph/algorithms/connectivity/
decompose.rs

1//! Decompose a graph into its (weakly) connected components (ALGO-CC-003).
2//!
3//! Counterpart of `igraph_decompose()` from
4//! `references/igraph/src/connectivity/components.c:603` — Phase-1
5//! minimal slice covers the `IGRAPH_WEAK` branch only, which is
6//! upstream's auto-selected mode for undirected graphs (see
7//! `igraph_i_decompose_weak` at `components.c:620`).
8//!
9//! Each component becomes a freshly built [`Graph`] with its vertices
10//! renumbered to `0..k` in BFS-from-`actstart` order, exactly matching
11//! upstream's `igraph_i_induced_subgraph_map(..., IGRAPH_SUBGRAPH_AUTO)`
12//! contract. Loops and parallel edges are preserved verbatim. Strong
13//! decomposition is a follow-up AWU.
14
15use std::collections::VecDeque;
16
17use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
18
19/// Decompose `graph` into its weakly connected components.
20///
21/// Returns one new [`Graph`] per component; the order matches the
22/// natural BFS-from-lowest-unvisited-vertex traversal used by
23/// `igraph_decompose(_, _, IGRAPH_WEAK, _, _)` (`components.c:620`).
24/// The component graph's vertex IDs are remapped to `0..k`, where `k`
25/// is the size of the component, in BFS visit order from the seed
26/// vertex. Edges within the component (loops and parallel edges
27/// included) are preserved with their original direction.
28///
29/// On directed input the decomposition still uses *weak* connectivity
30/// (edge direction is ignored when discovering components, but each
31/// component graph is built as directed and retains the original edge
32/// orientations). Strong decomposition is a follow-up AWU.
33///
34/// # Examples
35///
36/// ```
37/// use rust_igraph::{Graph, decompose};
38///
39/// // Two components: {0, 1, 2} (triangle) and {3, 4} (edge).
40/// let mut g = Graph::with_vertices(5);
41/// g.add_edge(0, 1).unwrap();
42/// g.add_edge(1, 2).unwrap();
43/// g.add_edge(2, 0).unwrap();
44/// g.add_edge(3, 4).unwrap();
45///
46/// let parts = decompose(&g).unwrap();
47/// assert_eq!(parts.len(), 2);
48/// // Triangle: 3 vertices, 3 edges.
49/// assert_eq!(parts[0].vcount(), 3);
50/// assert_eq!(parts[0].ecount(), 3);
51/// // Edge component: 2 vertices, 1 edge.
52/// assert_eq!(parts[1].vcount(), 2);
53/// assert_eq!(parts[1].ecount(), 1);
54/// ```
55pub fn decompose(graph: &Graph) -> IgraphResult<Vec<Graph>> {
56    let n = graph.vcount();
57    if n == 0 {
58        return Ok(Vec::new());
59    }
60
61    // BFS-discovery order, remembered per vertex via `vid_in_comp`
62    // (== local id within its component, or u32::MAX if unvisited).
63    let mut vid_in_comp = vec![u32::MAX; n as usize];
64    let mut comp_of = vec![u32::MAX; n as usize];
65    // Per-component vertex lists, indexed in BFS-discovery order.
66    let mut comp_vertices: Vec<Vec<VertexId>> = Vec::new();
67    let mut queue: VecDeque<VertexId> = VecDeque::new();
68
69    for actstart in 0..n {
70        if vid_in_comp[actstart as usize] != u32::MAX {
71            continue;
72        }
73        let comp_id = u32::try_from(comp_vertices.len())
74            .map_err(|_| IgraphError::Internal("too many components in decompose"))?;
75        let mut verts: Vec<VertexId> = Vec::new();
76
77        vid_in_comp[actstart as usize] = 0;
78        comp_of[actstart as usize] = comp_id;
79        verts.push(actstart);
80        queue.clear();
81        queue.push_back(actstart);
82
83        while let Some(v) = queue.pop_front() {
84            for w in graph.neighbors_iter(v)? {
85                if vid_in_comp[w as usize] == u32::MAX {
86                    let new_local = u32::try_from(verts.len())
87                        .map_err(|_| IgraphError::Internal("component too large in decompose"))?;
88                    vid_in_comp[w as usize] = new_local;
89                    comp_of[w as usize] = comp_id;
90                    verts.push(w);
91                    queue.push_back(w);
92                }
93            }
94        }
95        comp_vertices.push(verts);
96    }
97
98    // Pre-construct an empty Graph per component (matching directedness).
99    let directed = graph.is_directed();
100    let mut parts: Vec<Graph> = comp_vertices
101        .iter()
102        .map(|verts| {
103            // verts.len() ≤ vcount, fits u32 because vcount is u32.
104            let k = u32::try_from(verts.len())
105                .map_err(|_| IgraphError::Internal("component vertex count exceeds u32"))?;
106            Graph::new(k, directed)
107        })
108        .collect::<IgraphResult<Vec<_>>>()?;
109
110    // Single edge sweep: each edge belongs to exactly one weak
111    // component, so we can place it directly in the right subgraph
112    // using the `comp_of[s]` lookup. Endpoints are renumbered via
113    // `vid_in_comp[v]` into the component's local id space.
114    let m = graph.ecount();
115    for e in 0..m {
116        let eid = u32::try_from(e)
117            .map_err(|_| IgraphError::Internal("edge id exceeds u32 in decompose"))?;
118        let s = graph.edge_source(eid)?;
119        let t = graph.edge_target(eid)?;
120        let comp_s = comp_of[s as usize];
121        // For weak components both endpoints share the same comp id.
122        debug_assert_eq!(comp_s, comp_of[t as usize]);
123        let local_s = vid_in_comp[s as usize];
124        let local_t = vid_in_comp[t as usize];
125        parts[comp_s as usize].add_edge(local_s, local_t)?;
126    }
127
128    Ok(parts)
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn empty_graph_yields_no_components() {
137        let g = Graph::with_vertices(0);
138        let parts = decompose(&g).unwrap();
139        assert!(parts.is_empty());
140    }
141
142    #[test]
143    fn null_graph_with_isolated_vertices() {
144        let g = Graph::with_vertices(3);
145        let parts = decompose(&g).unwrap();
146        assert_eq!(parts.len(), 3);
147        for p in &parts {
148            assert_eq!(p.vcount(), 1);
149            assert_eq!(p.ecount(), 0);
150        }
151    }
152
153    #[test]
154    fn single_component_preserves_size() {
155        let mut g = Graph::with_vertices(4);
156        g.add_edge(0, 1).unwrap();
157        g.add_edge(1, 2).unwrap();
158        g.add_edge(2, 3).unwrap();
159        let parts = decompose(&g).unwrap();
160        assert_eq!(parts.len(), 1);
161        assert_eq!(parts[0].vcount(), 4);
162        assert_eq!(parts[0].ecount(), 3);
163    }
164
165    #[test]
166    fn two_disjoint_triangles() {
167        let mut g = Graph::with_vertices(6);
168        g.add_edge(0, 1).unwrap();
169        g.add_edge(1, 2).unwrap();
170        g.add_edge(2, 0).unwrap();
171        g.add_edge(3, 4).unwrap();
172        g.add_edge(4, 5).unwrap();
173        g.add_edge(5, 3).unwrap();
174        let parts = decompose(&g).unwrap();
175        assert_eq!(parts.len(), 2);
176        for p in &parts {
177            assert_eq!(p.vcount(), 3);
178            assert_eq!(p.ecount(), 3);
179        }
180    }
181
182    #[test]
183    fn vertex_remapping_starts_at_zero() {
184        // Triangle {3, 4, 5} (no vertices 0, 1, 2 connected). Result
185        // graph must have vertices 0, 1, 2 (renumbered).
186        let mut g = Graph::with_vertices(6);
187        g.add_edge(3, 4).unwrap();
188        g.add_edge(4, 5).unwrap();
189        g.add_edge(5, 3).unwrap();
190        let parts = decompose(&g).unwrap();
191        // 3 isolated singletons (0, 1, 2) + the triangle {3,4,5}.
192        assert_eq!(parts.len(), 4);
193        // First three are singletons (BFS-from-0, then 1, then 2).
194        for p in parts.iter().take(3) {
195            assert_eq!(p.vcount(), 1);
196            assert_eq!(p.ecount(), 0);
197        }
198        // Last is the triangle, with renumbered ids 0..2.
199        let tri = &parts[3];
200        assert_eq!(tri.vcount(), 3);
201        assert_eq!(tri.ecount(), 3);
202        // BFS-from-3 visits 3 (→ local 0), then 4 (→ 1), then 5 (→ 2).
203        assert_eq!(tri.edge_source(0).unwrap(), 0);
204        assert_eq!(tri.edge_target(0).unwrap(), 1);
205    }
206
207    #[test]
208    fn directed_graph_preserves_edge_orientation() {
209        // 0 → 1 → 2 (chain) + 3 isolated. Weak components: {0,1,2},
210        // {3}. The chain component's edges keep their direction.
211        let mut g = Graph::new(4, true).unwrap();
212        g.add_edge(0, 1).unwrap();
213        g.add_edge(1, 2).unwrap();
214        let parts = decompose(&g).unwrap();
215        assert_eq!(parts.len(), 2);
216        let chain = &parts[0];
217        assert!(chain.is_directed());
218        assert_eq!(chain.vcount(), 3);
219        assert_eq!(chain.ecount(), 2);
220        assert_eq!(chain.edge_source(0).unwrap(), 0);
221        assert_eq!(chain.edge_target(0).unwrap(), 1);
222        assert_eq!(chain.edge_source(1).unwrap(), 1);
223        assert_eq!(chain.edge_target(1).unwrap(), 2);
224    }
225
226    #[test]
227    fn loops_and_parallel_edges_preserved() {
228        let mut g = Graph::with_vertices(3);
229        g.add_edge(0, 0).unwrap(); // self-loop
230        g.add_edge(0, 1).unwrap();
231        g.add_edge(0, 1).unwrap(); // parallel
232        let parts = decompose(&g).unwrap();
233        // Vertex 2 is isolated → second component.
234        assert_eq!(parts.len(), 2);
235        let main = &parts[0];
236        assert_eq!(main.vcount(), 2);
237        assert_eq!(main.ecount(), 3);
238    }
239
240    #[test]
241    fn component_count_matches_connected_components() {
242        // Cross-check against ALGO-CC-001 on a multi-component graph.
243        let mut g = Graph::with_vertices(7);
244        g.add_edge(0, 1).unwrap();
245        g.add_edge(2, 3).unwrap();
246        g.add_edge(3, 4).unwrap();
247        // Vertex 5, 6 isolated.
248        let parts = decompose(&g).unwrap();
249        let cc = crate::algorithms::connectivity::components::connected_components(&g).unwrap();
250        assert_eq!(u32::try_from(parts.len()).unwrap(), cc.count);
251    }
252
253    #[test]
254    fn round_trip_edge_count_total_matches() {
255        // The total edge count across all subgraphs must equal the
256        // input edge count (every edge belongs to exactly one weak
257        // component).
258        let mut g = Graph::with_vertices(8);
259        g.add_edge(0, 1).unwrap();
260        g.add_edge(1, 2).unwrap();
261        g.add_edge(3, 4).unwrap();
262        g.add_edge(4, 5).unwrap();
263        g.add_edge(6, 7).unwrap();
264        let parts = decompose(&g).unwrap();
265        let total_e: usize = parts.iter().map(Graph::ecount).sum();
266        assert_eq!(total_e, g.ecount());
267        let total_v: u32 = parts.iter().map(Graph::vcount).sum();
268        assert_eq!(total_v, g.vcount());
269    }
270}