Skip to main content

oximedia_graph/
cycle_detect.rs

1#![allow(dead_code)]
2//! Cycle detection algorithms for directed graphs.
3//!
4//! This module provides multiple cycle detection strategies including
5//! DFS-based coloring, path-based, and incremental cycle detection
6//! for maintaining DAG invariants when edges are added.
7
8use std::collections::{HashMap, HashSet, VecDeque};
9
10/// A node identifier for cycle detection graphs.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
12pub struct CycleNodeId(
13    /// Inner identifier value.
14    pub usize,
15);
16
17impl std::fmt::Display for CycleNodeId {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        write!(f, "CycleNode({})", self.0)
20    }
21}
22
23/// Node coloring state used during DFS traversal.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum NodeColor {
26    /// Node has not been visited.
27    White,
28    /// Node is currently being explored (on the DFS stack).
29    Gray,
30    /// Node exploration is complete.
31    Black,
32}
33
34/// Represents a detected cycle as a sequence of node IDs.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct Cycle {
37    /// The nodes forming the cycle, in traversal order.
38    pub nodes: Vec<CycleNodeId>,
39}
40
41impl Cycle {
42    /// Create a new cycle from a list of nodes.
43    pub fn new(nodes: Vec<CycleNodeId>) -> Self {
44        Self { nodes }
45    }
46
47    /// Return the length of the cycle (number of edges).
48    pub fn len(&self) -> usize {
49        self.nodes.len()
50    }
51
52    /// Check if the cycle is empty.
53    pub fn is_empty(&self) -> bool {
54        self.nodes.is_empty()
55    }
56
57    /// Check if a node is part of this cycle.
58    pub fn contains(&self, id: CycleNodeId) -> bool {
59        self.nodes.contains(&id)
60    }
61
62    /// Return a canonical form of the cycle (starting from the smallest node).
63    pub fn canonical(&self) -> Self {
64        if self.nodes.is_empty() {
65            return self.clone();
66        }
67        let min_pos = self
68            .nodes
69            .iter()
70            .enumerate()
71            .min_by_key(|(_, n)| n.0)
72            .map(|(i, _)| i)
73            .unwrap_or(0);
74        let mut rotated = Vec::with_capacity(self.nodes.len());
75        rotated.extend_from_slice(&self.nodes[min_pos..]);
76        rotated.extend_from_slice(&self.nodes[..min_pos]);
77        Self { nodes: rotated }
78    }
79}
80
81impl std::fmt::Display for Cycle {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        let ids: Vec<String> = self.nodes.iter().map(|n| n.0.to_string()).collect();
84        write!(f, "[{}]", ids.join(" -> "))
85    }
86}
87
88/// Result of a cycle detection analysis.
89#[derive(Debug, Clone)]
90pub struct CycleDetectionResult {
91    /// Whether the graph is acyclic.
92    pub is_acyclic: bool,
93    /// All cycles found (may be empty if only checking existence).
94    pub cycles: Vec<Cycle>,
95    /// Number of nodes visited during detection.
96    pub nodes_visited: usize,
97}
98
99impl CycleDetectionResult {
100    /// Create a result indicating no cycles.
101    pub fn acyclic(nodes_visited: usize) -> Self {
102        Self {
103            is_acyclic: true,
104            cycles: Vec::new(),
105            nodes_visited,
106        }
107    }
108
109    /// Create a result with detected cycles.
110    pub fn with_cycles(cycles: Vec<Cycle>, nodes_visited: usize) -> Self {
111        Self {
112            is_acyclic: cycles.is_empty(),
113            cycles,
114            nodes_visited,
115        }
116    }
117
118    /// Return the total number of cycles found.
119    pub fn cycle_count(&self) -> usize {
120        self.cycles.len()
121    }
122}
123
124/// Directed graph for cycle detection.
125pub struct CycleGraph {
126    /// Adjacency list.
127    adjacency: HashMap<CycleNodeId, HashSet<CycleNodeId>>,
128}
129
130impl CycleGraph {
131    /// Create a new empty graph.
132    pub fn new() -> Self {
133        Self {
134            adjacency: HashMap::new(),
135        }
136    }
137
138    /// Add a node to the graph.
139    pub fn add_node(&mut self, id: CycleNodeId) {
140        self.adjacency.entry(id).or_default();
141    }
142
143    /// Add a directed edge from `from` to `to`.
144    pub fn add_edge(&mut self, from: CycleNodeId, to: CycleNodeId) {
145        self.add_node(from);
146        self.add_node(to);
147        self.adjacency.entry(from).or_default().insert(to);
148    }
149
150    /// Remove a directed edge.
151    pub fn remove_edge(&mut self, from: CycleNodeId, to: CycleNodeId) -> bool {
152        self.adjacency.get_mut(&from).is_some_and(|s| s.remove(&to))
153    }
154
155    /// Return the number of nodes.
156    pub fn node_count(&self) -> usize {
157        self.adjacency.len()
158    }
159
160    /// Return the number of edges.
161    pub fn edge_count(&self) -> usize {
162        self.adjacency.values().map(|s| s.len()).sum()
163    }
164
165    /// Check if the graph has any cycles using DFS coloring.
166    pub fn has_cycle(&self) -> bool {
167        let mut colors: HashMap<CycleNodeId, NodeColor> = self
168            .adjacency
169            .keys()
170            .map(|&k| (k, NodeColor::White))
171            .collect();
172
173        let mut nodes: Vec<CycleNodeId> = self.adjacency.keys().copied().collect();
174        nodes.sort();
175
176        for &node in &nodes {
177            if colors[&node] == NodeColor::White && self.dfs_has_cycle(node, &mut colors) {
178                return true;
179            }
180        }
181        false
182    }
183
184    /// DFS helper for cycle detection.
185    fn dfs_has_cycle(
186        &self,
187        node: CycleNodeId,
188        colors: &mut HashMap<CycleNodeId, NodeColor>,
189    ) -> bool {
190        colors.insert(node, NodeColor::Gray);
191
192        if let Some(successors) = self.adjacency.get(&node) {
193            for &succ in successors {
194                match colors.get(&succ) {
195                    Some(NodeColor::Gray) => return true,
196                    Some(NodeColor::White) => {
197                        if self.dfs_has_cycle(succ, colors) {
198                            return true;
199                        }
200                    }
201                    _ => {}
202                }
203            }
204        }
205
206        colors.insert(node, NodeColor::Black);
207        false
208    }
209
210    /// Detect all cycles in the graph using DFS.
211    pub fn detect_all_cycles(&self) -> CycleDetectionResult {
212        let mut visited: HashSet<CycleNodeId> = HashSet::new();
213        let mut rec_stack: HashSet<CycleNodeId> = HashSet::new();
214        let mut path: Vec<CycleNodeId> = Vec::new();
215        let mut cycles: Vec<Cycle> = Vec::new();
216        let mut nodes_visited = 0_usize;
217
218        let mut nodes: Vec<CycleNodeId> = self.adjacency.keys().copied().collect();
219        nodes.sort();
220
221        for &node in &nodes {
222            if !visited.contains(&node) {
223                self.dfs_find_cycles(
224                    node,
225                    &mut visited,
226                    &mut rec_stack,
227                    &mut path,
228                    &mut cycles,
229                    &mut nodes_visited,
230                );
231            }
232        }
233
234        CycleDetectionResult::with_cycles(cycles, nodes_visited)
235    }
236
237    /// DFS helper to find all cycles.
238    fn dfs_find_cycles(
239        &self,
240        node: CycleNodeId,
241        visited: &mut HashSet<CycleNodeId>,
242        rec_stack: &mut HashSet<CycleNodeId>,
243        path: &mut Vec<CycleNodeId>,
244        cycles: &mut Vec<Cycle>,
245        nodes_visited: &mut usize,
246    ) {
247        visited.insert(node);
248        rec_stack.insert(node);
249        path.push(node);
250        *nodes_visited += 1;
251
252        if let Some(successors) = self.adjacency.get(&node) {
253            let mut sorted_succ: Vec<CycleNodeId> = successors.iter().copied().collect();
254            sorted_succ.sort();
255            for succ in sorted_succ {
256                if rec_stack.contains(&succ) {
257                    // Found a cycle - extract it from the path
258                    if let Some(start_idx) = path.iter().position(|&n| n == succ) {
259                        let cycle_nodes = path[start_idx..].to_vec();
260                        cycles.push(Cycle::new(cycle_nodes));
261                    }
262                } else if !visited.contains(&succ) {
263                    self.dfs_find_cycles(succ, visited, rec_stack, path, cycles, nodes_visited);
264                }
265            }
266        }
267
268        path.pop();
269        rec_stack.remove(&node);
270    }
271
272    /// Check if adding an edge from `from` to `to` would create a cycle.
273    /// This performs a BFS from `to` to see if it can reach `from`.
274    pub fn would_create_cycle(&self, from: CycleNodeId, to: CycleNodeId) -> bool {
275        if from == to {
276            return true;
277        }
278
279        let mut visited: HashSet<CycleNodeId> = HashSet::new();
280        let mut queue: VecDeque<CycleNodeId> = VecDeque::new();
281        queue.push_back(to);
282
283        while let Some(current) = queue.pop_front() {
284            if current == from {
285                return true;
286            }
287            if visited.insert(current) {
288                if let Some(successors) = self.adjacency.get(&current) {
289                    for &succ in successors {
290                        queue.push_back(succ);
291                    }
292                }
293            }
294        }
295
296        false
297    }
298
299    /// Add an edge only if it would not create a cycle.
300    /// Returns true if the edge was added, false if it would create a cycle.
301    pub fn try_add_edge(&mut self, from: CycleNodeId, to: CycleNodeId) -> bool {
302        if self.would_create_cycle(from, to) {
303            return false;
304        }
305        self.add_edge(from, to);
306        true
307    }
308
309    /// Return all nodes that are part of at least one cycle.
310    pub fn cyclic_nodes(&self) -> HashSet<CycleNodeId> {
311        let result = self.detect_all_cycles();
312        let mut cyclic: HashSet<CycleNodeId> = HashSet::new();
313        for cycle in &result.cycles {
314            for &node in &cycle.nodes {
315                cyclic.insert(node);
316            }
317        }
318        cyclic
319    }
320
321    /// Check if a specific node is part of any cycle.
322    pub fn is_in_cycle(&self, id: CycleNodeId) -> bool {
323        self.cyclic_nodes().contains(&id)
324    }
325}
326
327impl Default for CycleGraph {
328    fn default() -> Self {
329        Self::new()
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    fn cn(id: usize) -> CycleNodeId {
338        CycleNodeId(id)
339    }
340
341    #[test]
342    fn test_empty_graph_no_cycles() {
343        let graph = CycleGraph::new();
344        assert!(!graph.has_cycle());
345    }
346
347    #[test]
348    fn test_single_node_no_cycle() {
349        let mut graph = CycleGraph::new();
350        graph.add_node(cn(0));
351        assert!(!graph.has_cycle());
352    }
353
354    #[test]
355    fn test_dag_no_cycle() {
356        let mut graph = CycleGraph::new();
357        graph.add_edge(cn(0), cn(1));
358        graph.add_edge(cn(1), cn(2));
359        graph.add_edge(cn(0), cn(2));
360        assert!(!graph.has_cycle());
361    }
362
363    #[test]
364    fn test_simple_cycle() {
365        let mut graph = CycleGraph::new();
366        graph.add_edge(cn(0), cn(1));
367        graph.add_edge(cn(1), cn(2));
368        graph.add_edge(cn(2), cn(0));
369        assert!(graph.has_cycle());
370    }
371
372    #[test]
373    fn test_self_loop() {
374        let mut graph = CycleGraph::new();
375        graph.add_edge(cn(0), cn(0));
376        assert!(graph.has_cycle());
377    }
378
379    #[test]
380    fn test_detect_all_cycles() {
381        let mut graph = CycleGraph::new();
382        graph.add_edge(cn(0), cn(1));
383        graph.add_edge(cn(1), cn(0));
384        let result = graph.detect_all_cycles();
385        assert!(!result.is_acyclic);
386        assert!(!result.cycles.is_empty());
387    }
388
389    #[test]
390    fn test_detect_acyclic() {
391        let mut graph = CycleGraph::new();
392        graph.add_edge(cn(0), cn(1));
393        graph.add_edge(cn(1), cn(2));
394        let result = graph.detect_all_cycles();
395        assert!(result.is_acyclic);
396        assert_eq!(result.cycle_count(), 0);
397    }
398
399    #[test]
400    fn test_would_create_cycle() {
401        let mut graph = CycleGraph::new();
402        graph.add_edge(cn(0), cn(1));
403        graph.add_edge(cn(1), cn(2));
404        assert!(graph.would_create_cycle(cn(2), cn(0)));
405        assert!(!graph.would_create_cycle(cn(0), cn(2)));
406    }
407
408    #[test]
409    fn test_would_create_self_loop() {
410        let graph = CycleGraph::new();
411        assert!(graph.would_create_cycle(cn(0), cn(0)));
412    }
413
414    #[test]
415    fn test_try_add_edge_success() {
416        let mut graph = CycleGraph::new();
417        graph.add_edge(cn(0), cn(1));
418        assert!(graph.try_add_edge(cn(1), cn(2)));
419        assert_eq!(graph.edge_count(), 2);
420    }
421
422    #[test]
423    fn test_try_add_edge_rejected() {
424        let mut graph = CycleGraph::new();
425        graph.add_edge(cn(0), cn(1));
426        graph.add_edge(cn(1), cn(2));
427        assert!(!graph.try_add_edge(cn(2), cn(0)));
428        assert_eq!(graph.edge_count(), 2);
429    }
430
431    #[test]
432    fn test_cyclic_nodes() {
433        let mut graph = CycleGraph::new();
434        graph.add_edge(cn(0), cn(1));
435        graph.add_edge(cn(1), cn(2));
436        graph.add_edge(cn(2), cn(0));
437        graph.add_edge(cn(3), cn(0)); // Node 3 is not in the cycle
438        let cyclic = graph.cyclic_nodes();
439        assert!(cyclic.contains(&cn(0)));
440        assert!(cyclic.contains(&cn(1)));
441        assert!(cyclic.contains(&cn(2)));
442        assert!(!cyclic.contains(&cn(3)));
443    }
444
445    #[test]
446    fn test_is_in_cycle() {
447        let mut graph = CycleGraph::new();
448        graph.add_edge(cn(0), cn(1));
449        graph.add_edge(cn(1), cn(0));
450        graph.add_node(cn(2));
451        assert!(graph.is_in_cycle(cn(0)));
452        assert!(!graph.is_in_cycle(cn(2)));
453    }
454
455    #[test]
456    fn test_remove_edge() {
457        let mut graph = CycleGraph::new();
458        graph.add_edge(cn(0), cn(1));
459        assert!(graph.remove_edge(cn(0), cn(1)));
460        assert!(!graph.remove_edge(cn(0), cn(1)));
461        assert_eq!(graph.edge_count(), 0);
462    }
463
464    #[test]
465    fn test_cycle_canonical() {
466        let cycle = Cycle::new(vec![cn(2), cn(0), cn(1)]);
467        let canonical = cycle.canonical();
468        assert_eq!(canonical.nodes[0], cn(0));
469    }
470
471    #[test]
472    fn test_cycle_display() {
473        let cycle = Cycle::new(vec![cn(0), cn(1), cn(2)]);
474        let display = format!("{cycle}");
475        assert!(display.contains("0"));
476        assert!(display.contains("2"));
477    }
478
479    #[test]
480    fn test_cycle_contains() {
481        let cycle = Cycle::new(vec![cn(0), cn(1), cn(2)]);
482        assert!(cycle.contains(cn(1)));
483        assert!(!cycle.contains(cn(5)));
484    }
485
486    #[test]
487    fn test_node_color_states() {
488        assert_eq!(NodeColor::White, NodeColor::White);
489        assert_ne!(NodeColor::White, NodeColor::Gray);
490        assert_ne!(NodeColor::Gray, NodeColor::Black);
491    }
492
493    #[test]
494    fn test_cycle_detection_result_acyclic() {
495        let result = CycleDetectionResult::acyclic(10);
496        assert!(result.is_acyclic);
497        assert_eq!(result.nodes_visited, 10);
498        assert_eq!(result.cycle_count(), 0);
499    }
500}