Skip to main content

facett_graphview/analysis/
components.rs

1//! **Connected components** — undirected (union-find) + strongly-connected
2//! (Tarjan, directed). Both return a labelling `Vec<usize>` (`comp[i]` = component id,
3//! compact `0..count`) plus the component `count`.
4
5use super::Adjacency;
6
7/// A component labelling: `of[i]` is node `i`'s component (compact `0..count`).
8#[derive(Clone, Debug, PartialEq)]
9pub struct Components {
10    pub of: Vec<usize>,
11    pub count: usize,
12}
13
14impl Components {
15    /// Group node indices by component, ordered by component id.
16    #[must_use]
17    pub fn groups(&self) -> Vec<Vec<usize>> {
18        let mut g = vec![Vec::new(); self.count];
19        for (i, &c) in self.of.iter().enumerate() {
20            g[c].push(i);
21        }
22        g
23    }
24    /// Size of the largest component (`0` when empty).
25    #[must_use]
26    pub fn largest(&self) -> usize {
27        self.groups().iter().map(Vec::len).max().unwrap_or(0)
28    }
29}
30
31/// **Weakly / undirected connected components** via union-find. Two nodes share a
32/// component iff a path connects them ignoring edge direction.
33#[must_use]
34pub fn connected(g: &Adjacency) -> Components {
35    let n = g.n;
36    let mut uf = UnionFind::new(n);
37    for i in 0..n {
38        for &(j, _) in &g.und[i] {
39            uf.union(i, j);
40        }
41    }
42    compactify(&(0..n).map(|i| uf.find(i)).collect::<Vec<_>>())
43}
44
45/// **Strongly connected components** (Tarjan, directed). Two nodes share an SCC iff
46/// each is reachable from the other following edge direction. Iterative (no recursion
47/// blowout on deep graphs). Component ids are compact `0..count`.
48#[must_use]
49pub fn strongly_connected(g: &Adjacency) -> Components {
50    let n = g.n;
51    let mut index = vec![usize::MAX; n];
52    let mut low = vec![0usize; n];
53    let mut on_stack = vec![false; n];
54    let mut stack: Vec<usize> = Vec::new();
55    let mut comp = vec![usize::MAX; n];
56    let mut next_index = 0usize;
57    let mut next_comp = 0usize;
58
59    // Iterative Tarjan: a work stack of (node, next-neighbour-cursor).
60    for start in 0..n {
61        if index[start] != usize::MAX {
62            continue;
63        }
64        let mut work: Vec<(usize, usize)> = vec![(start, 0)];
65        while let Some(&(v, ci)) = work.last() {
66            if ci == 0 {
67                index[v] = next_index;
68                low[v] = next_index;
69                next_index += 1;
70                stack.push(v);
71                on_stack[v] = true;
72            }
73            if ci < g.out[v].len() {
74                // Advance this frame's cursor, then recurse / relax the child.
75                work.last_mut().unwrap().1 += 1;
76                let w = g.out[v][ci].0;
77                if index[w] == usize::MAX {
78                    work.push((w, 0));
79                } else if on_stack[w] {
80                    low[v] = low[v].min(index[w]);
81                }
82            } else {
83                // Done with v: if it's a root, pop its SCC.
84                if low[v] == index[v] {
85                    loop {
86                        let w = stack.pop().unwrap();
87                        on_stack[w] = false;
88                        comp[w] = next_comp;
89                        if w == v {
90                            break;
91                        }
92                    }
93                    next_comp += 1;
94                }
95                work.pop();
96                if let Some(&(p, _)) = work.last() {
97                    low[p] = low[p].min(low[v]);
98                }
99            }
100        }
101    }
102    // Tarjan emits SCCs in reverse-topological order; relabel to compact ids as-is.
103    Components { of: comp, count: next_comp }
104}
105
106/// Relabel arbitrary root ids into compact `0..count` (stable by first appearance).
107fn compactify(roots: &[usize]) -> Components {
108    let mut map = std::collections::HashMap::new();
109    let mut of = Vec::with_capacity(roots.len());
110    let mut count = 0;
111    for &r in roots {
112        let id = *map.entry(r).or_insert_with(|| {
113            let id = count;
114            count += 1;
115            id
116        });
117        of.push(id);
118    }
119    Components { of, count }
120}
121
122/// Union-find with path compression + union by size.
123struct UnionFind {
124    parent: Vec<usize>,
125    size: Vec<usize>,
126}
127
128impl UnionFind {
129    fn new(n: usize) -> Self {
130        Self { parent: (0..n).collect(), size: vec![1; n] }
131    }
132    fn find(&mut self, x: usize) -> usize {
133        let mut r = x;
134        while self.parent[r] != r {
135            r = self.parent[r];
136        }
137        // Path compression.
138        let mut c = x;
139        while self.parent[c] != r {
140            let nxt = self.parent[c];
141            self.parent[c] = r;
142            c = nxt;
143        }
144        r
145    }
146    fn union(&mut self, a: usize, b: usize) {
147        let (ra, rb) = (self.find(a), self.find(b));
148        if ra == rb {
149            return;
150        }
151        let (big, small) = if self.size[ra] >= self.size[rb] { (ra, rb) } else { (rb, ra) };
152        self.parent[small] = big;
153        self.size[big] += self.size[small];
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn connected_finds_two_islands() {
163        // {0,1,2} linked; {3,4} linked; separate.
164        let g = Adjacency::from_edges(5, &[(0, 1), (1, 2), (3, 4)]);
165        let c = connected(&g);
166        assert_eq!(c.count, 2);
167        assert_eq!(c.of[0], c.of[2], "0 and 2 are in one component");
168        assert_ne!(c.of[0], c.of[3], "the islands are distinct");
169        assert_eq!(c.largest(), 3);
170    }
171
172    #[test]
173    fn scc_collapses_a_cycle_but_splits_a_chain() {
174        // 0→1→2→0 is one SCC; 3→4 splits into two singletons.
175        let g = Adjacency::from_edges(5, &[(0, 1), (1, 2), (2, 0), (3, 4)]);
176        let scc = strongly_connected(&g);
177        assert_eq!(scc.of[0], scc.of[1]);
178        assert_eq!(scc.of[1], scc.of[2], "the 3-cycle is one SCC");
179        assert_ne!(scc.of[3], scc.of[4], "an acyclic chain is two singleton SCCs");
180        assert_eq!(scc.count, 3, "one cycle-SCC + two singletons");
181    }
182
183    #[test]
184    fn scc_of_a_dag_is_all_singletons() {
185        let g = Adjacency::from_edges(4, &[(0, 1), (1, 2), (2, 3), (0, 3)]);
186        let scc = strongly_connected(&g);
187        assert_eq!(scc.count, 4, "a DAG has no non-trivial SCC");
188    }
189
190    #[test]
191    fn empty_graph_has_isolated_components() {
192        let g = Adjacency::from_edges(3, &[]);
193        assert_eq!(connected(&g).count, 3);
194        assert_eq!(strongly_connected(&g).count, 3);
195    }
196}