Skip to main content

oxicuda_graphalg/dynamic/
incremental_scc.rs

1//! Incremental strongly-connected-component maintenance under edge updates.
2//!
3//! [`IncrementalScc`] tracks the SCC partition of a [`DynamicGraph`] while edges
4//! are inserted and deleted one (or a batch) at a time, recomputing only the
5//! part of the structure that an update can actually change. The key structural
6//! facts exploited are:
7//!
8//! * **Insertion `u → v` can only *merge* SCCs, never split one.** A merge
9//!   happens exactly when `v` could already reach `u` (the new edge then closes
10//!   a directed cycle). The SCCs that coalesce are precisely those lying on some
11//!   `comp(v) ⇝ comp(u)` path in the *condensation* DAG — i.e. the components
12//!   reachable from `comp(v)` **and** co-reachable to `comp(u)`. We find that set
13//!   by a forward search from `comp(v)` and a backward search to `comp(u)` over
14//!   the condensation and fuse the intersection into one component.
15//!   When `v` cannot reach `u`, the partition is unchanged (the common case),
16//!   and the update costs only the reachability probe — no global recomputation.
17//!
18//! * **Deletion `u → v` can only *split* an SCC, never merge.** It can affect at
19//!   most the single component `C = comp(u)`, and only when `comp(u) == comp(v)`.
20//!   We then re-run Tarjan **restricted to the induced subgraph on `C`** and
21//!   replace `C` with the resulting sub-components. A deletion of a
22//!   cross-component edge changes nothing.
23//!
24//! Both paths are exact: after any sequence of updates the maintained partition
25//! equals a from-scratch [`scc_tarjan`]
26//! recomputation on the current graph (verified in the test suite over randomised
27//! update streams). Component identifiers are dense in `0..num_components()` and
28//! are renumbered after structural changes.
29
30use std::collections::VecDeque;
31
32use crate::connectivity::scc_tarjan::scc_tarjan;
33use crate::dynamic::dynamic_graph::DynamicGraph;
34use crate::error::{GraphalgError, GraphalgResult};
35
36/// Incremental SCC tracker over a mutable directed graph.
37#[derive(Debug, Clone)]
38pub struct IncrementalScc {
39    graph: DynamicGraph,
40    /// `comp_of[v]` = dense component id of vertex `v` in `0..num_comp`.
41    comp_of: Vec<usize>,
42    /// Members of each component (`members[c]` is the vertex list of comp `c`).
43    members: Vec<Vec<usize>>,
44}
45
46impl IncrementalScc {
47    /// Build the tracker, computing the initial SCC partition with Tarjan.
48    pub fn new(graph: DynamicGraph) -> GraphalgResult<Self> {
49        let n = graph.num_nodes();
50        let mut me = Self {
51            graph,
52            comp_of: vec![0; n],
53            members: Vec::new(),
54        };
55        me.full_recompute()?;
56        Ok(me)
57    }
58
59    /// Number of vertices.
60    pub fn num_nodes(&self) -> usize {
61        self.graph.num_nodes()
62    }
63
64    /// Number of strongly-connected components.
65    pub fn num_components(&self) -> usize {
66        self.members.len()
67    }
68
69    /// Dense component id of vertex `v`.
70    pub fn component_of(&self, v: usize) -> GraphalgResult<usize> {
71        if v >= self.graph.num_nodes() {
72            return Err(GraphalgError::IndexOutOfBounds {
73                index: v,
74                len: self.graph.num_nodes(),
75            });
76        }
77        Ok(self.comp_of[v])
78    }
79
80    /// Whether `u` and `v` are in the same SCC (mutually reachable).
81    pub fn same_component(&self, u: usize, v: usize) -> GraphalgResult<bool> {
82        Ok(self.component_of(u)? == self.component_of(v)?)
83    }
84
85    /// The component partition as sorted vertex lists, each list sorted and the
86    /// outer list ordered by smallest member.
87    pub fn components(&self) -> Vec<Vec<usize>> {
88        let mut out: Vec<Vec<usize>> = self
89            .members
90            .iter()
91            .map(|m| {
92                let mut v = m.clone();
93                v.sort_unstable();
94                v
95            })
96            .collect();
97        out.sort_by_key(|c| c.first().copied().unwrap_or(usize::MAX));
98        out
99    }
100
101    /// Borrow the underlying graph.
102    pub fn graph(&self) -> &DynamicGraph {
103        &self.graph
104    }
105
106    /// Insert directed edge `u → v` and update the partition incrementally.
107    pub fn apply_insert(&mut self, u: usize, v: usize) -> GraphalgResult<()> {
108        self.graph.add_edge(u, v)?;
109        self.handle_insert(u, v)
110    }
111
112    /// Remove directed edge `u → v` and update the partition incrementally.
113    ///
114    /// Removing an edge never changes any vertex's *current* component label, so
115    /// the affected-component test inside `handle_remove`
116    /// can safely read `comp_of` after the graph mutation.
117    pub fn apply_remove(&mut self, u: usize, v: usize) -> GraphalgResult<()> {
118        self.graph.remove_edge(u, v)?;
119        self.handle_remove(u, v)
120    }
121
122    /// Apply a batch of `(u, v, is_insert)` updates, maintaining the partition
123    /// after each one.
124    pub fn apply_batch(&mut self, updates: &[(usize, usize, bool)]) -> GraphalgResult<()> {
125        for &(u, v, is_insert) in updates {
126            if is_insert {
127                self.apply_insert(u, v)?;
128            } else {
129                self.apply_remove(u, v)?;
130            }
131        }
132        Ok(())
133    }
134
135    // --- internal machinery -------------------------------------------------
136
137    /// Recompute the whole partition from scratch (used at construction).
138    fn full_recompute(&mut self) -> GraphalgResult<()> {
139        let g = self.graph.to_adjacency_list();
140        let sccs = scc_tarjan(&g)?;
141        self.install_partition(sccs);
142        Ok(())
143    }
144
145    /// Install a fresh list of component vertex-lists, assigning dense ids.
146    fn install_partition(&mut self, sccs: Vec<Vec<usize>>) {
147        let n = self.graph.num_nodes();
148        let mut comp_of = vec![usize::MAX; n];
149        let mut members: Vec<Vec<usize>> = Vec::with_capacity(sccs.len());
150        for comp in sccs {
151            let id = members.len();
152            for &v in &comp {
153                comp_of[v] = id;
154            }
155            members.push(comp);
156        }
157        // Tarjan returns every vertex exactly once, so `comp_of` is fully set.
158        self.comp_of = comp_of;
159        self.members = members;
160    }
161
162    /// Insertion handler: merge the components on a `comp(v) ⇝ comp(u)` cycle.
163    fn handle_insert(&mut self, u: usize, v: usize) -> GraphalgResult<()> {
164        let cu = self.comp_of[u];
165        let cv = self.comp_of[v];
166        if cu == cv {
167            // Endpoints already in the same SCC: nothing changes.
168            return Ok(());
169        }
170        // Components reachable from `cv` in the condensation that can also reach
171        // `cu` form the merge set. If `cu` is not forward-reachable from `cv`
172        // there is no new cycle and the partition is unchanged.
173        let merge = self.condensation_cycle_components(cv, cu);
174        if merge.len() <= 1 {
175            return Ok(());
176        }
177        self.merge_components(&merge);
178        Ok(())
179    }
180
181    /// Deletion handler: re-split `comp(u)` if `u` and `v` shared a component.
182    fn handle_remove(&mut self, u: usize, v: usize) -> GraphalgResult<()> {
183        let n = self.graph.num_nodes();
184        if u >= n || v >= n {
185            return Ok(());
186        }
187        let cu = self.comp_of[u];
188        let cv = self.comp_of[v];
189        if cu != cv {
190            // Cross-component edge removal cannot change SCC structure.
191            return Ok(());
192        }
193        // Re-run Tarjan on the induced subgraph of the affected component only.
194        let component = self.members[cu].clone();
195        let sub = self.tarjan_on_subset(&component)?;
196        if sub.len() == 1 {
197            // Still one SCC: membership unchanged.
198            return Ok(());
199        }
200        self.replace_component(cu, sub);
201        Ok(())
202    }
203
204    /// Set of condensation nodes reachable from `start_comp` that can also reach
205    /// `target_comp`. Empty/singleton when `target_comp` is not reachable from
206    /// `start_comp`. Operates over the *current* partition; condensation edges are
207    /// derived on the fly from `graph` + `comp_of`.
208    fn condensation_cycle_components(&self, start_comp: usize, target_comp: usize) -> Vec<usize> {
209        let nc = self.members.len();
210        // Forward reachability from start_comp over condensation edges.
211        let forward = self.condensation_reach(start_comp, false);
212        if !forward[target_comp] {
213            return Vec::new();
214        }
215        // Backward reachability to target_comp (forward over reversed edges).
216        let backward = self.condensation_reach(target_comp, true);
217        let mut out = Vec::new();
218        for c in 0..nc {
219            if forward[c] && backward[c] {
220                out.push(c);
221            }
222        }
223        out
224    }
225
226    /// BFS over the condensation from `src`. When `reverse` is true, traverse
227    /// condensation edges backwards (i.e. predecessors). Self-loops are ignored;
228    /// the source is always marked reachable.
229    fn condensation_reach(&self, src: usize, reverse: bool) -> Vec<bool> {
230        let nc = self.members.len();
231        let mut seen = vec![false; nc];
232        if src >= nc {
233            return seen;
234        }
235        seen[src] = true;
236        let mut queue = VecDeque::new();
237        queue.push_back(src);
238        while let Some(c) = queue.pop_front() {
239            // Enumerate condensation neighbours of component `c`.
240            for &x in &self.members[c] {
241                let neigh = if reverse {
242                    self.graph.in_neighbors(x)
243                } else {
244                    self.graph.out_neighbors(x)
245                };
246                if let Ok(list) = neigh {
247                    for &y in list {
248                        let cy = self.comp_of[y];
249                        if cy != c && !seen[cy] {
250                            seen[cy] = true;
251                            queue.push_back(cy);
252                        }
253                    }
254                }
255            }
256        }
257        seen
258    }
259
260    /// Fuse every component id in `merge` into a single component, then renumber
261    /// all components densely. `merge` must contain at least two distinct ids.
262    fn merge_components(&mut self, merge: &[usize]) {
263        let mut keep = vec![true; self.members.len()];
264        let mut fused: Vec<usize> = Vec::new();
265        for &c in merge {
266            keep[c] = false;
267            fused.extend(std::mem::take(&mut self.members[c]));
268        }
269        // Rebuild the member list: surviving components first, then the fused one.
270        let mut new_members: Vec<Vec<usize>> = Vec::with_capacity(self.members.len());
271        for c in 0..self.members.len() {
272            if keep[c] {
273                new_members.push(std::mem::take(&mut self.members[c]));
274            }
275        }
276        new_members.push(fused);
277        self.reinstall(new_members);
278    }
279
280    /// Replace component `cid` with the sub-components `sub` (each a vertex list),
281    /// then renumber densely.
282    fn replace_component(&mut self, cid: usize, sub: Vec<Vec<usize>>) {
283        let mut new_members: Vec<Vec<usize>> = Vec::with_capacity(self.members.len() + sub.len());
284        for c in 0..self.members.len() {
285            if c == cid {
286                continue;
287            }
288            new_members.push(std::mem::take(&mut self.members[c]));
289        }
290        for s in sub {
291            new_members.push(s);
292        }
293        self.reinstall(new_members);
294    }
295
296    /// Reassign dense component ids from a member list and rebuild `comp_of`.
297    fn reinstall(&mut self, members: Vec<Vec<usize>>) {
298        let n = self.graph.num_nodes();
299        let mut comp_of = vec![usize::MAX; n];
300        for (id, comp) in members.iter().enumerate() {
301            for &v in comp {
302                comp_of[v] = id;
303            }
304        }
305        self.comp_of = comp_of;
306        self.members = members;
307    }
308
309    /// Run Tarjan on the subgraph induced by `subset` (using only edges with
310    /// both endpoints inside the subset), returning the sub-SCCs as vertex lists
311    /// in the *original* vertex numbering.
312    fn tarjan_on_subset(&self, subset: &[usize]) -> GraphalgResult<Vec<Vec<usize>>> {
313        // Map subset vertices to a compact 0..k index space.
314        let k = subset.len();
315        let mut local = vec![usize::MAX; self.graph.num_nodes()];
316        for (i, &v) in subset.iter().enumerate() {
317            local[v] = i;
318        }
319        // Build a small AdjacencyList over the compact space.
320        let mut sub = crate::repr::adjacency_list::AdjacencyList::new(k);
321        for &v in subset {
322            let lv = local[v];
323            for &w in self.graph.out_neighbors(v)? {
324                let lw = local[w];
325                if lw != usize::MAX {
326                    // multiplicity is irrelevant to reachability/SCC structure
327                    sub.add_edge(lv, lw)?;
328                }
329            }
330        }
331        let local_sccs = scc_tarjan(&sub)?;
332        // Translate back to original ids.
333        let translated = local_sccs
334            .into_iter()
335            .map(|comp| comp.into_iter().map(|li| subset[li]).collect::<Vec<_>>())
336            .collect();
337        Ok(translated)
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344    use crate::handle::LcgRng;
345
346    /// Canonicalise a partition (sorted vertex lists, sorted by first member) for
347    /// equality comparison.
348    fn canon(mut parts: Vec<Vec<usize>>) -> Vec<Vec<usize>> {
349        for p in &mut parts {
350            p.sort_unstable();
351        }
352        parts.sort_by_key(|p| p.first().copied().unwrap_or(usize::MAX));
353        parts
354    }
355
356    /// Ground-truth partition via from-scratch Tarjan on the dynamic graph.
357    fn ground_truth(g: &DynamicGraph) -> Vec<Vec<usize>> {
358        canon(scc_tarjan(&g.to_adjacency_list()).expect("tarjan"))
359    }
360
361    #[test]
362    fn insertion_merges_into_cycle() {
363        // 0 -> 1 -> 2 ; then add 2 -> 0 to form one SCC.
364        let mut g = DynamicGraph::new(3);
365        g.add_edge(0, 1).expect("e");
366        g.add_edge(1, 2).expect("e");
367        let mut scc = IncrementalScc::new(g).expect("new");
368        assert_eq!(scc.num_components(), 3);
369        scc.apply_insert(2, 0).expect("insert");
370        assert_eq!(scc.num_components(), 1);
371        assert!(scc.same_component(0, 2).expect("same"));
372        assert_eq!(scc.components(), ground_truth(scc.graph()));
373    }
374
375    #[test]
376    fn insertion_without_cycle_keeps_partition() {
377        let mut g = DynamicGraph::new(4);
378        g.add_edge(0, 1).expect("e");
379        g.add_edge(2, 3).expect("e");
380        let mut scc = IncrementalScc::new(g).expect("new");
381        let before = scc.num_components();
382        scc.apply_insert(1, 2).expect("insert"); // bridges but no cycle
383        assert_eq!(scc.num_components(), before);
384        assert_eq!(scc.components(), ground_truth(scc.graph()));
385    }
386
387    #[test]
388    fn deletion_splits_cycle() {
389        // 0 <-> 1 <-> 2 cycle 0->1->2->0; remove 2->0 splits it.
390        let mut g = DynamicGraph::new(3);
391        g.add_edge(0, 1).expect("e");
392        g.add_edge(1, 2).expect("e");
393        g.add_edge(2, 0).expect("e");
394        let mut scc = IncrementalScc::new(g).expect("new");
395        assert_eq!(scc.num_components(), 1);
396        scc.apply_remove(2, 0).expect("remove");
397        assert_eq!(scc.num_components(), 3);
398        assert_eq!(scc.components(), ground_truth(scc.graph()));
399    }
400
401    #[test]
402    fn deletion_of_cross_edge_noop() {
403        let mut g = DynamicGraph::new(4);
404        g.add_edge(0, 1).expect("e");
405        g.add_edge(1, 0).expect("e"); // {0,1} SCC
406        g.add_edge(1, 2).expect("e"); // cross edge
407        g.add_edge(2, 3).expect("e");
408        let mut scc = IncrementalScc::new(g).expect("new");
409        let before = scc.components();
410        scc.apply_remove(1, 2).expect("remove");
411        assert_eq!(scc.components(), before);
412        assert_eq!(scc.components(), ground_truth(scc.graph()));
413    }
414
415    #[test]
416    fn deletion_keeps_scc_when_alternate_path() {
417        // Triangle 0->1->2->0 plus chord 0->2 means removing 0->1... still need
418        // the cycle. Use 0->1->2->0 and a parallel 1->2 so removing one 1->2 keeps
419        // the SCC intact (multiplicity).
420        let mut g = DynamicGraph::new(3);
421        g.add_edge(0, 1).expect("e");
422        g.add_edge(1, 2).expect("e");
423        g.add_edge(1, 2).expect("e"); // parallel
424        g.add_edge(2, 0).expect("e");
425        let mut scc = IncrementalScc::new(g).expect("new");
426        assert_eq!(scc.num_components(), 1);
427        scc.apply_remove(1, 2).expect("remove"); // still one 1->2 left
428        assert_eq!(scc.num_components(), 1, "parallel edge keeps SCC");
429        assert_eq!(scc.components(), ground_truth(scc.graph()));
430    }
431
432    #[test]
433    fn merge_of_two_existing_sccs() {
434        // Two 2-cycles {0,1} and {2,3}; an edge 1->2 then 3->0 merges all four.
435        let mut g = DynamicGraph::new(4);
436        g.add_edge(0, 1).expect("e");
437        g.add_edge(1, 0).expect("e");
438        g.add_edge(2, 3).expect("e");
439        g.add_edge(3, 2).expect("e");
440        let mut scc = IncrementalScc::new(g).expect("new");
441        assert_eq!(scc.num_components(), 2);
442        scc.apply_insert(1, 2).expect("insert"); // no cycle yet
443        assert_eq!(scc.num_components(), 2);
444        scc.apply_insert(3, 0).expect("insert"); // now {0,1,2,3}
445        assert_eq!(scc.num_components(), 1);
446        assert_eq!(scc.components(), ground_truth(scc.graph()));
447    }
448
449    #[test]
450    fn random_update_stream_matches_tarjan() {
451        let mut rng = LcgRng::new(0x5EED_1234);
452        for trial in 0..40 {
453            let n = 4 + rng.next_usize(7); // 4..=10
454            let mut scc = IncrementalScc::new(DynamicGraph::new(n)).expect("new");
455            // Track present distinct edges to issue legal removals.
456            let mut present: Vec<(usize, usize)> = Vec::new();
457            for step in 0..60 {
458                let do_insert = present.is_empty() || rng.next_bool();
459                if do_insert {
460                    let u = rng.next_usize(n);
461                    let mut v = rng.next_usize(n);
462                    if v == u {
463                        v = (v + 1) % n;
464                    }
465                    scc.apply_insert(u, v).expect("insert");
466                    if !present.contains(&(u, v)) {
467                        present.push((u, v));
468                    }
469                } else {
470                    let idx = rng.next_usize(present.len());
471                    let (u, v) = present[idx];
472                    scc.apply_remove(u, v).expect("remove");
473                    // multiplicity is always one in this stream, so it's gone
474                    present.swap_remove(idx);
475                }
476                assert_eq!(
477                    scc.components(),
478                    ground_truth(scc.graph()),
479                    "mismatch trial {trial} step {step} (n={n})"
480                );
481                // comp_of must be consistent with components()
482                for v in 0..n {
483                    let cid = scc.component_of(v).expect("comp");
484                    assert!(scc.members[cid].contains(&v));
485                }
486            }
487        }
488    }
489
490    #[test]
491    fn batch_update_matches_tarjan() {
492        let mut g = DynamicGraph::new(5);
493        g.add_edge(0, 1).expect("e");
494        g.add_edge(1, 2).expect("e");
495        let mut scc = IncrementalScc::new(g).expect("new");
496        let updates = [
497            (2, 0, true),  // form {0,1,2}
498            (3, 4, true),  //
499            (4, 3, true),  // form {3,4}
500            (2, 3, true),  // cross, no merge
501            (0, 1, false), // remove one of the cycle edges
502        ];
503        scc.apply_batch(&updates).expect("batch");
504        assert_eq!(scc.components(), ground_truth(scc.graph()));
505    }
506
507    #[test]
508    fn self_loop_is_its_own_scc_member() {
509        let mut g = DynamicGraph::new(2);
510        g.add_edge(0, 0).expect("e"); // self-loop
511        g.add_edge(0, 1).expect("e");
512        let scc = IncrementalScc::new(g).expect("new");
513        assert_eq!(scc.num_components(), 2);
514        assert_eq!(scc.components(), ground_truth(scc.graph()));
515    }
516
517    #[test]
518    fn component_of_out_of_range_errors() {
519        let scc = IncrementalScc::new(DynamicGraph::new(2)).expect("new");
520        assert!(scc.component_of(5).is_err());
521    }
522}