Skip to main content

helix_graph_algorithms/algorithms/
cycles.rs

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/// Bounded cycle-enumeration options.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub struct CycleOptions {
12    /// Maximum number of edges in a returned cycle.
13    pub length_bound: NonZeroUsize,
14    /// Optional output cap.
15    pub max_cycles: Option<NonZeroUsize>,
16}
17
18/// One canonical simple cycle. The first node is not repeated at the end.
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct Cycle {
21    /// Canonically rotated node sequence.
22    pub node_ids: Vec<NodeId>,
23    /// One representative edge per adjacent node pair, including the closing
24    /// edge from the last node to the first.
25    pub edge_ids: Vec<EdgeId>,
26}
27
28/// Bounded cycle output and truncation state.
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct CycleResult {
31    /// Cycles in canonical deterministic order.
32    pub cycles: Vec<Cycle>,
33    /// True only when `max_cycles` stopped enumeration.
34    pub truncated: bool,
35}
36
37impl Graph {
38    /// Enumerate bounded simple cycles with in-search pruning.
39    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}