flow_rs_core/graph/
traversal.rs

1//! Graph traversal algorithms
2
3use std::collections::{HashMap, HashSet, VecDeque};
4
5use crate::error::{FlowError, Result};
6use crate::types::NodeId;
7
8use super::Graph;
9
10impl<N, E> Graph<N, E>
11where
12    N: Clone,
13    E: Clone,
14{
15    /// Check if the graph contains any cycles using DFS-based cycle detection
16    ///
17    /// Uses Depth-First Search with recursion stack tracking to detect back edges.
18    /// Time complexity: O(V + E), Space complexity: O(V)
19    pub fn has_cycle(&self) -> bool {
20        let mut visited = HashSet::new();
21        let mut rec_stack = HashSet::new();
22
23        // Check each node as a potential starting point
24        for node_id in self.node_ids() {
25            if !visited.contains(node_id)
26                && self.has_cycle_dfs(node_id, &mut visited, &mut rec_stack)
27            {
28                return true;
29            }
30        }
31
32        false
33    }
34
35    /// DFS helper for cycle detection
36    fn has_cycle_dfs(
37        &self,
38        node_id: &NodeId,
39        visited: &mut HashSet<NodeId>,
40        rec_stack: &mut HashSet<NodeId>,
41    ) -> bool {
42        visited.insert(node_id.clone());
43        rec_stack.insert(node_id.clone());
44
45        // Check all neighbors (nodes this node points to)
46        // Optimize: only iterate through edges that start from this node
47        for edge in self.get_outgoing_edges(node_id) {
48            let neighbor = &edge.target;
49
50            // If neighbor not visited, recurse
51            if !visited.contains(neighbor) {
52                if self.has_cycle_dfs(neighbor, visited, rec_stack) {
53                    return true;
54                }
55            }
56            // If neighbor is in recursion stack, we found a back edge (cycle)
57            else if rec_stack.contains(neighbor) {
58                return true;
59            }
60        }
61
62        rec_stack.remove(node_id);
63        false
64    }
65
66    /// Check if adding an edge from source to target would create a cycle
67    ///
68    /// This is useful for preventing cycles during interactive edge creation.
69    /// Time complexity: O(V + E), Space complexity: O(V)
70    pub fn creates_cycle(&self, source: &NodeId, target: &NodeId) -> bool {
71        // If nodes don't exist, no cycle can be created
72        if !self.nodes.contains_key(source) || !self.nodes.contains_key(target) {
73            return false;
74        }
75
76        // Check if target can reach source (would create cycle if we add source -> target)
77        self.can_reach(target, source)
78    }
79
80    /// Check if 'from' node can reach 'to' node through existing edges
81    fn can_reach(&self, from: &NodeId, to: &NodeId) -> bool {
82        if from == to {
83            return true;
84        }
85
86        let mut visited = HashSet::new();
87        let mut queue = VecDeque::new();
88
89        queue.push_back(from.clone());
90        visited.insert(from.clone());
91
92        while let Some(current) = queue.pop_front() {
93            // Check all outgoing edges from current node
94            // Optimize: only iterate through edges that start from this node
95            for edge in self.get_outgoing_edges(&current) {
96                let neighbor = &edge.target;
97
98                if neighbor == to {
99                    return true;
100                }
101
102                if !visited.contains(neighbor) {
103                    visited.insert(neighbor.clone());
104                    queue.push_back(neighbor.clone());
105                }
106            }
107        }
108
109        false
110    }
111
112    /// Find a cycle in the graph, returning the cycle path if found
113    ///
114    /// Returns the first cycle found, or None if the graph is acyclic.
115    /// The returned path represents the nodes in the cycle.
116    /// Time complexity: O(V + E), Space complexity: O(V)
117    pub fn find_cycle(&self) -> Option<Vec<NodeId>> {
118        let mut visited = HashSet::new();
119        let mut rec_stack = HashSet::new();
120        let mut parent = HashMap::new();
121
122        // Check each node as potential starting point
123        for node_id in self.node_ids() {
124            if !visited.contains(node_id) {
125                if let Some(cycle) =
126                    self.find_cycle_dfs(node_id, &mut visited, &mut rec_stack, &mut parent)
127                {
128                    return Some(cycle);
129                }
130            }
131        }
132
133        None
134    }
135
136    /// DFS helper for finding cycle path
137    fn find_cycle_dfs(
138        &self,
139        node_id: &NodeId,
140        visited: &mut HashSet<NodeId>,
141        rec_stack: &mut HashSet<NodeId>,
142        parent: &mut HashMap<NodeId, NodeId>,
143    ) -> Option<Vec<NodeId>> {
144        visited.insert(node_id.clone());
145        rec_stack.insert(node_id.clone());
146
147        // Check all neighbors
148        // Optimize: only iterate through edges that start from this node
149        for edge in self.get_outgoing_edges(node_id) {
150            let neighbor = &edge.target;
151
152            // If neighbor not visited, recurse
153            if !visited.contains(neighbor) {
154                parent.insert(neighbor.clone(), node_id.clone());
155                if let Some(cycle) = self.find_cycle_dfs(neighbor, visited, rec_stack, parent) {
156                    return Some(cycle);
157                }
158            }
159            // If neighbor is in recursion stack, we found a cycle
160            else if rec_stack.contains(neighbor) {
161                // Reconstruct cycle path
162                let mut cycle = vec![neighbor.clone()];
163                let mut current = node_id.clone();
164
165                // Walk back through parents until we reach the cycle start
166                while current != *neighbor {
167                    cycle.push(current.clone());
168                    current = parent.get(&current).unwrap_or(&current).clone();
169                }
170
171                cycle.reverse();
172                return Some(cycle);
173            }
174        }
175
176        rec_stack.remove(node_id);
177        None
178    }
179
180    /// Perform topological sort on the graph using Kahn's algorithm
181    ///
182    /// Returns a valid topological ordering of nodes, or Err if the graph contains cycles.
183    /// A topological sort is a linear ordering where for every directed edge (u, v),
184    /// vertex u comes before v in the ordering.
185    /// Time complexity: O(V + E), Space complexity: O(V)
186    pub fn topological_sort(&self) -> Result<Vec<NodeId>> {
187        // Calculate in-degrees
188        let mut in_degree: HashMap<NodeId, usize> = HashMap::new();
189
190        // Initialize all nodes with in-degree 0
191        for node_id in self.node_ids() {
192            in_degree.insert(node_id.clone(), 0);
193        }
194
195        // Count incoming edges for each node
196        for edge in self.edges() {
197            *in_degree.entry(edge.target.clone()).or_insert(0) += 1;
198        }
199
200        // Find nodes with in-degree 0
201        let mut queue = VecDeque::new();
202        for (node_id, &degree) in &in_degree {
203            if degree == 0 {
204                queue.push_back(node_id.clone());
205            }
206        }
207
208        let mut result = Vec::new();
209
210        while let Some(node_id) = queue.pop_front() {
211            result.push(node_id.clone());
212
213            // Reduce in-degree of neighbors
214            for edge in self.edges() {
215                if edge.source == node_id {
216                    let neighbor = &edge.target;
217                    if let Some(degree) = in_degree.get_mut(neighbor) {
218                        *degree -= 1;
219                        if *degree == 0 {
220                            queue.push_back(neighbor.clone());
221                        }
222                    }
223                }
224            }
225        }
226
227        // If we didn't process all nodes, there must be a cycle
228        if result.len() != self.node_count() {
229            return Err(FlowError::invalid_operation(
230                "Graph contains cycles - topological sort not possible",
231            ));
232        }
233
234        Ok(result)
235    }
236}