1use std::collections::{BTreeMap, BTreeSet, VecDeque};
2use std::num::NonZeroUsize;
3
4use serde::{Deserialize, Serialize};
5
6use super::TraversalDirection;
7use crate::{EdgeId, Graph, NodeId};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub struct CycleOptions {
12 pub length_bound: NonZeroUsize,
14 pub max_cycles: Option<NonZeroUsize>,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct Cycle {
21 pub node_ids: Vec<NodeId>,
23 pub edge_ids: Vec<EdgeId>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct CycleResult {
31 pub cycles: Vec<Cycle>,
33 pub truncated: bool,
35}
36
37impl Graph {
38 pub fn simple_cycles(&self, options: CycleOptions) -> CycleResult {
40 let components = if self.is_directed() {
41 self.strongly_connected_components()
42 } else {
43 self.connected_components()
44 };
45 let mut canonical = BTreeMap::<Vec<NodeId>, Vec<EdgeId>>::new();
46 let mut truncated = false;
47 for component in components {
48 if component.len() == 1 {
49 let node = component[0];
50 let has_self_loop = self
51 .arcs(node, TraversalDirection::Out)
52 .any(|arc| arc.neighbor == node);
53 if !has_self_loop {
54 continue;
55 }
56 }
57 let component_set = component.iter().copied().collect::<BTreeSet<_>>();
58 for start in &component {
59 let mut in_path = vec![false; self.node_count()];
60 in_path[*start] = true;
61 let mut path_nodes = vec![*start];
62 let mut path_edges = Vec::new();
63 if self.enumerate_cycles_from(
64 *start,
65 *start,
66 &component_set,
67 options,
68 &mut in_path,
69 &mut path_nodes,
70 &mut path_edges,
71 &mut canonical,
72 ) {
73 truncated = true;
74 break;
75 }
76 }
77 if truncated {
78 break;
79 }
80 }
81 CycleResult {
82 cycles: canonical
83 .into_iter()
84 .map(|(node_ids, edge_ids)| Cycle { node_ids, edge_ids })
85 .collect(),
86 truncated,
87 }
88 }
89
90 #[allow(clippy::too_many_arguments)]
91 fn enumerate_cycles_from(
92 &self,
93 start: usize,
94 current: usize,
95 component: &BTreeSet<usize>,
96 options: CycleOptions,
97 in_path: &mut [bool],
98 path_nodes: &mut Vec<usize>,
99 path_edges: &mut Vec<usize>,
100 canonical: &mut BTreeMap<Vec<NodeId>, Vec<EdgeId>>,
101 ) -> bool {
102 let direction = if self.is_directed() {
103 TraversalDirection::Out
104 } else {
105 TraversalDirection::Both
106 };
107 for arc in self.arcs(current, direction) {
108 if !component.contains(&arc.neighbor) || path_edges.contains(&arc.edge) {
109 continue;
110 }
111 if arc.neighbor == start {
112 let cycle_len = path_edges.len() + 1;
113 if cycle_len <= options.length_bound.get() {
114 path_edges.push(arc.edge);
115 let (nodes, edges) = self.canonical_cycle(path_nodes, path_edges);
116 match canonical.get_mut(&nodes) {
117 Some(existing) if edges < *existing => *existing = edges,
118 Some(_) => {}
119 None => {
120 canonical.insert(nodes, edges);
121 }
122 }
123 path_edges.pop();
124 if options
125 .max_cycles
126 .is_some_and(|limit| canonical.len() >= limit.get())
127 {
128 return true;
129 }
130 }
131 continue;
132 }
133 if in_path[arc.neighbor] || path_edges.len() + 1 >= options.length_bound.get() {
134 continue;
135 }
136 in_path[arc.neighbor] = true;
137 path_nodes.push(arc.neighbor);
138 path_edges.push(arc.edge);
139 let stop = self.enumerate_cycles_from(
140 start,
141 arc.neighbor,
142 component,
143 options,
144 in_path,
145 path_nodes,
146 path_edges,
147 canonical,
148 );
149 path_edges.pop();
150 path_nodes.pop();
151 in_path[arc.neighbor] = false;
152 if stop {
153 return true;
154 }
155 }
156 false
157 }
158
159 fn canonical_cycle(
160 &self,
161 path_nodes: &[usize],
162 path_edges: &[usize],
163 ) -> (Vec<NodeId>, Vec<EdgeId>) {
164 let nodes = path_nodes
165 .iter()
166 .map(|node| self.node_id(*node).clone())
167 .collect::<Vec<_>>();
168 let edges = path_edges
169 .iter()
170 .map(|edge| self.edge_at(*edge).id.clone())
171 .collect::<Vec<_>>();
172 let mut candidates = rotations(&nodes, &edges);
173 if !self.is_directed() {
174 let reverse_nodes = nodes.iter().cloned().rev().collect::<Vec<_>>();
175 let edge_count = edges.len();
176 let reverse_edges = (0..edge_count)
177 .map(|index| edges[(edge_count + edge_count - 2 - index) % edge_count].clone())
178 .collect::<Vec<_>>();
179 candidates.extend(rotations(&reverse_nodes, &reverse_edges));
180 }
181 candidates
182 .into_iter()
183 .min()
184 .expect("a cycle always has at least one rotation")
185 }
186
187 fn connected_components(&self) -> Vec<Vec<usize>> {
188 let mut visited = vec![false; self.node_count()];
189 let mut components = Vec::new();
190 for start in 0..self.node_count() {
191 if visited[start] {
192 continue;
193 }
194 visited[start] = true;
195 let mut queue = VecDeque::from([start]);
196 let mut component = Vec::new();
197 while let Some(node) = queue.pop_front() {
198 component.push(node);
199 for arc in self.arcs(node, TraversalDirection::Both) {
200 if !visited[arc.neighbor] {
201 visited[arc.neighbor] = true;
202 queue.push_back(arc.neighbor);
203 }
204 }
205 }
206 components.push(component);
207 }
208 components
209 }
210
211 fn strongly_connected_components(&self) -> Vec<Vec<usize>> {
212 struct Tarjan {
213 next_index: usize,
214 indexes: Vec<Option<usize>>,
215 lowlinks: Vec<usize>,
216 stack: Vec<usize>,
217 on_stack: Vec<bool>,
218 components: Vec<Vec<usize>>,
219 }
220
221 fn visit(graph: &Graph, node: usize, state: &mut Tarjan) {
222 let node_index = state.next_index;
223 state.next_index += 1;
224 state.indexes[node] = Some(node_index);
225 state.lowlinks[node] = node_index;
226 state.stack.push(node);
227 state.on_stack[node] = true;
228 for arc in graph.arcs(node, TraversalDirection::Out) {
229 match state.indexes[arc.neighbor] {
230 None => {
231 visit(graph, arc.neighbor, state);
232 state.lowlinks[node] =
233 state.lowlinks[node].min(state.lowlinks[arc.neighbor]);
234 }
235 Some(index) if state.on_stack[arc.neighbor] => {
236 state.lowlinks[node] = state.lowlinks[node].min(index);
237 }
238 Some(_) => {}
239 }
240 }
241 if state.lowlinks[node] == node_index {
242 let mut component = Vec::new();
243 loop {
244 let member = state.stack.pop().expect("SCC root is on the Tarjan stack");
245 state.on_stack[member] = false;
246 component.push(member);
247 if member == node {
248 break;
249 }
250 }
251 component.sort_by(|left, right| graph.node_id(*left).cmp(graph.node_id(*right)));
252 state.components.push(component);
253 }
254 }
255
256 let mut state = Tarjan {
257 next_index: 0,
258 indexes: vec![None; self.node_count()],
259 lowlinks: vec![0; self.node_count()],
260 stack: Vec::new(),
261 on_stack: vec![false; self.node_count()],
262 components: Vec::new(),
263 };
264 for node in 0..self.node_count() {
265 if state.indexes[node].is_none() {
266 visit(self, node, &mut state);
267 }
268 }
269 state
270 .components
271 .sort_by(|left, right| self.node_id(left[0]).cmp(self.node_id(right[0])));
272 state.components
273 }
274}
275
276fn rotations(nodes: &[NodeId], edges: &[EdgeId]) -> Vec<(Vec<NodeId>, Vec<EdgeId>)> {
277 (0..nodes.len())
278 .map(|offset| {
279 (
280 nodes[offset..]
281 .iter()
282 .chain(nodes[..offset].iter())
283 .cloned()
284 .collect(),
285 edges[offset..]
286 .iter()
287 .chain(edges[..offset].iter())
288 .cloned()
289 .collect(),
290 )
291 })
292 .collect()
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use crate::{Edge, GraphKind, Node};
299
300 #[test]
301 fn directed_cycles_are_bounded_canonical_and_include_self_loops() {
302 let graph = Graph::new(
303 GraphKind::DiGraph,
304 [Node::new("a"), Node::new("b"), Node::new("c")],
305 [
306 Edge::new("aa", "a", "a"),
307 Edge::new("ab", "a", "b"),
308 Edge::new("ba", "b", "a"),
309 Edge::new("bc", "b", "c"),
310 Edge::new("ca", "c", "a"),
311 ],
312 )
313 .unwrap();
314 let result = graph.simple_cycles(CycleOptions {
315 length_bound: NonZeroUsize::new(2).unwrap(),
316 max_cycles: None,
317 });
318 assert_eq!(
319 result
320 .cycles
321 .iter()
322 .map(|cycle| cycle.node_ids.clone())
323 .collect::<Vec<_>>(),
324 [
325 vec!["a".to_string()],
326 vec!["a".to_string(), "b".to_string()]
327 ]
328 );
329 assert!(!result.truncated);
330 }
331
332 #[test]
333 fn cycle_limit_stops_enumeration() {
334 let graph = Graph::new(
335 GraphKind::DiGraph,
336 [Node::new("a"), Node::new("b")],
337 [Edge::new("aa", "a", "a"), Edge::new("bb", "b", "b")],
338 )
339 .unwrap();
340 let result = graph.simple_cycles(CycleOptions {
341 length_bound: NonZeroUsize::new(1).unwrap(),
342 max_cycles: NonZeroUsize::new(1),
343 });
344 assert_eq!(result.cycles.len(), 1);
345 assert!(result.truncated);
346 }
347
348 #[test]
349 fn undirected_reverse_cycles_deduplicate() {
350 let graph = Graph::new(
351 GraphKind::Graph,
352 [Node::new("a"), Node::new("b"), Node::new("c")],
353 [
354 Edge::new("ab", "a", "b"),
355 Edge::new("bc", "b", "c"),
356 Edge::new("ca", "c", "a"),
357 ],
358 )
359 .unwrap();
360 let result = graph.simple_cycles(CycleOptions {
361 length_bound: NonZeroUsize::new(3).unwrap(),
362 max_cycles: None,
363 });
364 assert!(result.cycles.iter().any(|cycle| cycle.node_ids.len() == 3));
365 }
366
367 #[test]
368 fn undirected_two_cycle_requires_distinct_parallel_edges() {
369 let simple = Graph::new(
370 GraphKind::Graph,
371 [Node::new("a"), Node::new("b")],
372 [Edge::new("ab", "a", "b")],
373 )
374 .unwrap();
375 assert!(simple
376 .simple_cycles(CycleOptions {
377 length_bound: NonZeroUsize::new(2).unwrap(),
378 max_cycles: None,
379 })
380 .cycles
381 .is_empty());
382
383 let parallel = Graph::new(
384 GraphKind::MultiGraph,
385 [Node::new("a"), Node::new("b")],
386 [Edge::new("one", "a", "b"), Edge::new("two", "a", "b")],
387 )
388 .unwrap();
389 let result = parallel.simple_cycles(CycleOptions {
390 length_bound: NonZeroUsize::new(2).unwrap(),
391 max_cycles: None,
392 });
393 assert_eq!(result.cycles.len(), 1);
394 assert_eq!(result.cycles[0].node_ids, ["a", "b"]);
395 assert_eq!(result.cycles[0].edge_ids.len(), 2);
396 }
397}