weavatrix_graph/algo/
cycles.rs1use super::strongly_connected_components_filtered;
2use crate::IndexGraphView;
3use crate::Vec;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct CycleEnumeration<Node> {
7 paths: Vec<Vec<Node>>,
8 truncated: bool,
9}
10
11impl<Node> CycleEnumeration<Node> {
12 #[must_use]
13 pub fn paths(&self) -> &[Vec<Node>] {
14 &self.paths
15 }
16
17 #[must_use]
18 pub const fn truncated(&self) -> bool {
19 self.truncated
20 }
21}
22
23pub fn johnson_cycles<G>(graph: &G, max_cycles: usize) -> CycleEnumeration<G::Node>
28where
29 G: IndexGraphView,
30{
31 if max_cycles == 0 {
32 return CycleEnumeration {
33 paths: Vec::new(),
34 truncated: false,
35 };
36 }
37 let (by_slot, adjacency) = indexed_adjacency(graph);
38 let mut state = JohnsonState::new(graph.node_bound(), max_cycles);
39 let mut lower = 0;
40 while let Some(component) = least_cyclic_component(graph, &adjacency, lower) {
41 let Some(start) = component.iter().min().copied() else {
42 break;
43 };
44 let mut allowed = vec![false; graph.node_bound()];
45 for &node in &component {
46 allowed[node] = true;
47 }
48 state.reset();
49 circuit(start, start, &adjacency, &allowed, &by_slot, &mut state);
50 if state.truncated {
51 break;
52 }
53 lower = start + 1;
54 }
55 CycleEnumeration {
56 paths: state.results,
57 truncated: state.truncated,
58 }
59}
60
61fn indexed_adjacency<G: IndexGraphView>(graph: &G) -> (Vec<Option<G::Node>>, Vec<Vec<usize>>) {
62 let mut by_slot = vec![None; graph.node_bound()];
63 let mut adjacency = vec![Vec::new(); graph.node_bound()];
64 for node in graph.node_indices() {
65 let source = G::node_slot(node);
66 by_slot[source] = Some(node);
67 adjacency[source] = graph
68 .outgoing_edges(node)
69 .filter_map(|edge| graph.edge_endpoints(edge))
70 .map(|endpoints| G::node_slot(endpoints.target()))
71 .collect();
72 adjacency[source].sort_unstable();
73 adjacency[source].dedup();
74 }
75 (by_slot, adjacency)
76}
77
78fn least_cyclic_component<G>(
79 graph: &G,
80 adjacency: &[Vec<usize>],
81 lower: usize,
82) -> Option<Vec<usize>>
83where
84 G: IndexGraphView,
85{
86 strongly_connected_components_filtered(graph, |edge| {
87 graph.edge_endpoints(edge).is_some_and(|endpoints| {
88 G::node_slot(endpoints.source()) >= lower && G::node_slot(endpoints.target()) >= lower
89 })
90 })
91 .into_iter()
92 .map(|component| component.into_iter().map(G::node_slot).collect::<Vec<_>>())
93 .filter(|component| {
94 component.iter().all(|node| *node >= lower)
95 && (component.len() > 1
96 || component
97 .first()
98 .is_some_and(|node| adjacency[*node].binary_search(node).is_ok()))
99 })
100 .min_by_key(|component| component.iter().min().copied())
101}
102
103struct JohnsonState<Node> {
104 blocked: Vec<bool>,
105 blocked_by: Vec<Vec<usize>>,
106 stack: Vec<usize>,
107 results: Vec<Vec<Node>>,
108 limit: usize,
109 truncated: bool,
110}
111
112impl<Node> JohnsonState<Node> {
113 fn new(bound: usize, limit: usize) -> Self {
114 Self {
115 blocked: vec![false; bound],
116 blocked_by: vec![Vec::new(); bound],
117 stack: Vec::new(),
118 results: Vec::new(),
119 limit,
120 truncated: false,
121 }
122 }
123
124 fn reset(&mut self) {
125 self.blocked.fill(false);
126 for dependencies in &mut self.blocked_by {
127 dependencies.clear();
128 }
129 self.stack.clear();
130 }
131}
132
133fn circuit<Node: Copy>(
134 node: usize,
135 start: usize,
136 adjacency: &[Vec<usize>],
137 allowed: &[bool],
138 by_slot: &[Option<Node>],
139 state: &mut JohnsonState<Node>,
140) -> bool {
141 let Some(start_node) = by_slot[start] else {
142 return false;
143 };
144 let mut found = false;
145 state.stack.push(node);
146 state.blocked[node] = true;
147 for &neighbor in &adjacency[node] {
148 if !allowed[neighbor] {
149 continue;
150 }
151 if neighbor == start {
152 if state.results.len() == state.limit {
153 state.truncated = true;
154 break;
155 }
156 let mut cycle = state
157 .stack
158 .iter()
159 .filter_map(|slot| by_slot[*slot])
160 .collect::<Vec<_>>();
161 cycle.push(start_node);
162 state.results.push(cycle);
163 found = true;
164 } else if !state.blocked[neighbor]
165 && circuit(neighbor, start, adjacency, allowed, by_slot, state)
166 {
167 found = true;
168 }
169 if state.truncated {
170 break;
171 }
172 }
173 if found {
174 unblock(node, state);
175 } else {
176 for &neighbor in &adjacency[node] {
177 if allowed[neighbor] && !state.blocked_by[neighbor].contains(&node) {
178 state.blocked_by[neighbor].push(node);
179 }
180 }
181 }
182 state.stack.pop();
183 found
184}
185
186fn unblock<Node>(node: usize, state: &mut JohnsonState<Node>) {
187 state.blocked[node] = false;
188 let dependencies = core::mem::take(&mut state.blocked_by[node]);
189 for dependency in dependencies {
190 if state.blocked[dependency] {
191 unblock(dependency, state);
192 }
193 }
194}