Skip to main content

geographdb_core/algorithms/
scc.rs

1//! Tarjan's Strongly Connected Components (SCC) Algorithm
2//!
3//! Finds maximal subgraphs where every node can reach every other node.
4
5use crate::algorithms::astar::CfgGraphNode;
6use std::collections::{HashMap, HashSet};
7
8/// Result of SCC analysis
9#[derive(Debug, Clone)]
10pub struct SccResult {
11    /// Each component is a set of node IDs that are mutually reachable
12    pub components: Vec<Vec<u64>>,
13    /// Maps each node to its component index
14    pub node_to_component: HashMap<u64, usize>,
15    /// Number of non-trivial SCCs (size > 1, indicates cycles)
16    pub cycle_count: usize,
17}
18
19struct TarjanState {
20    index: u64,
21    stack: Vec<u64>,
22    on_stack: HashSet<u64>,
23    indices: HashMap<u64, u64>,
24    lowlinks: HashMap<u64, u64>,
25    components: Vec<Vec<u64>>,
26}
27
28impl TarjanState {
29    fn new() -> Self {
30        Self {
31            index: 0,
32            stack: Vec::new(),
33            on_stack: HashSet::new(),
34            indices: HashMap::new(),
35            lowlinks: HashMap::new(),
36            components: Vec::new(),
37        }
38    }
39}
40
41/// Find strongly connected components using Tarjan's algorithm
42///
43/// # Complexity
44/// O(|V| + |E|) time, O(|V|) space
45pub fn tarjan_scc(nodes: &[CfgGraphNode]) -> SccResult {
46    let mut state = TarjanState::new();
47    let node_map: HashMap<u64, &CfgGraphNode> = nodes.iter().map(|n| (n.id, n)).collect();
48
49    for node in nodes {
50        if !state.indices.contains_key(&node.id) {
51            strongconnect(node.id, &node_map, &mut state);
52        }
53    }
54
55    let mut node_to_component = HashMap::new();
56    for (i, component) in state.components.iter().enumerate() {
57        for &node_id in component {
58            node_to_component.insert(node_id, i);
59        }
60    }
61
62    let cycle_count = state.components.iter().filter(|c| c.len() > 1).count();
63
64    SccResult {
65        components: state.components,
66        node_to_component,
67        cycle_count,
68    }
69}
70
71fn strongconnect(node_id: u64, node_map: &HashMap<u64, &CfgGraphNode>, state: &mut TarjanState) {
72    state.indices.insert(node_id, state.index);
73    state.lowlinks.insert(node_id, state.index);
74    state.index += 1;
75    state.stack.push(node_id);
76    state.on_stack.insert(node_id);
77
78    if let Some(node) = node_map.get(&node_id) {
79        for &successor_id in &node.successors {
80            if !state.indices.contains_key(&successor_id) {
81                strongconnect(successor_id, node_map, state);
82                let successor_lowlink = *state.lowlinks.get(&successor_id).unwrap();
83                let node_lowlink = state.lowlinks.get_mut(&node_id).unwrap();
84                *node_lowlink = (*node_lowlink).min(successor_lowlink);
85            } else if state.on_stack.contains(&successor_id) {
86                let successor_index = *state.indices.get(&successor_id).unwrap();
87                let node_lowlink = state.lowlinks.get_mut(&node_id).unwrap();
88                *node_lowlink = (*node_lowlink).min(successor_index);
89            }
90        }
91    }
92
93    let node_lowlink = *state.lowlinks.get(&node_id).unwrap();
94    let node_index = *state.indices.get(&node_id).unwrap();
95
96    if node_lowlink == node_index {
97        let mut component = Vec::new();
98        loop {
99            let w = state.stack.pop().unwrap();
100            state.on_stack.remove(&w);
101            component.push(w);
102            if w == node_id {
103                break;
104            }
105        }
106        state.components.push(component);
107    }
108}
109
110/// Find cycles in the graph (non-trivial SCCs)
111pub fn find_cycles(nodes: &[CfgGraphNode]) -> Vec<Vec<u64>> {
112    let result = tarjan_scc(nodes);
113    result
114        .components
115        .into_iter()
116        .filter(|c| c.len() > 1)
117        .collect()
118}
119
120/// Check if graph has any cycles
121pub fn has_cycles(nodes: &[CfgGraphNode]) -> bool {
122    let result = tarjan_scc(nodes);
123    result.cycle_count > 0
124}
125
126/// Get the condensed DAG (each SCC becomes a supernode)
127pub fn condense_graph(nodes: &[CfgGraphNode]) -> Vec<Vec<usize>> {
128    let result = tarjan_scc(nodes);
129    let num_components = result.components.len();
130
131    let mut condensed = vec![HashSet::new(); num_components];
132
133    for node in nodes {
134        let from_component = result.node_to_component.get(&node.id).copied().unwrap_or(0);
135        for &successor_id in &node.successors {
136            let to_component = result
137                .node_to_component
138                .get(&successor_id)
139                .copied()
140                .unwrap_or(0);
141            if from_component != to_component {
142                condensed[from_component].insert(to_component);
143            }
144        }
145    }
146
147    condensed
148        .into_iter()
149        .map(|s| s.into_iter().collect())
150        .collect()
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_tarjan_scc_no_cycles() {
159        let nodes = vec![
160            CfgGraphNode {
161                id: 0,
162                x: 0.0,
163                y: 0.0,
164                z: 0.0,
165                successors: vec![1],
166            },
167            CfgGraphNode {
168                id: 1,
169                x: 1.0,
170                y: 0.0,
171                z: 0.0,
172                successors: vec![2],
173            },
174            CfgGraphNode {
175                id: 2,
176                x: 2.0,
177                y: 0.0,
178                z: 0.0,
179                successors: vec![],
180            },
181        ];
182
183        let result = tarjan_scc(&nodes);
184        assert_eq!(result.components.len(), 3);
185        assert_eq!(result.cycle_count, 0);
186    }
187
188    #[test]
189    fn test_tarjan_scc_simple_cycle() {
190        let nodes = vec![
191            CfgGraphNode {
192                id: 0,
193                x: 0.0,
194                y: 0.0,
195                z: 0.0,
196                successors: vec![1],
197            },
198            CfgGraphNode {
199                id: 1,
200                x: 1.0,
201                y: 0.0,
202                z: 0.0,
203                successors: vec![2],
204            },
205            CfgGraphNode {
206                id: 2,
207                x: 2.0,
208                y: 0.0,
209                z: 0.0,
210                successors: vec![0],
211            },
212        ];
213
214        let result = tarjan_scc(&nodes);
215        assert_eq!(result.components.len(), 1);
216        assert_eq!(result.cycle_count, 1);
217    }
218
219    #[test]
220    fn test_has_cycles() {
221        let nodes_no = vec![
222            CfgGraphNode {
223                id: 0,
224                x: 0.0,
225                y: 0.0,
226                z: 0.0,
227                successors: vec![1],
228            },
229            CfgGraphNode {
230                id: 1,
231                x: 1.0,
232                y: 0.0,
233                z: 0.0,
234                successors: vec![],
235            },
236        ];
237        assert!(!has_cycles(&nodes_no));
238
239        let nodes_yes = vec![
240            CfgGraphNode {
241                id: 0,
242                x: 0.0,
243                y: 0.0,
244                z: 0.0,
245                successors: vec![1],
246            },
247            CfgGraphNode {
248                id: 1,
249                x: 1.0,
250                y: 0.0,
251                z: 0.0,
252                successors: vec![0],
253            },
254        ];
255        assert!(has_cycles(&nodes_yes));
256    }
257
258    #[test]
259    fn test_condense_graph() {
260        let nodes = vec![
261            CfgGraphNode {
262                id: 0,
263                x: 0.0,
264                y: 0.0,
265                z: 0.0,
266                successors: vec![1, 2],
267            },
268            CfgGraphNode {
269                id: 1,
270                x: 1.0,
271                y: 0.0,
272                z: 0.0,
273                successors: vec![0],
274            },
275            CfgGraphNode {
276                id: 2,
277                x: 2.0,
278                y: 0.0,
279                z: 0.0,
280                successors: vec![],
281            },
282        ];
283
284        let condensed = condense_graph(&nodes);
285        assert_eq!(condensed.len(), 2);
286    }
287}