1use std::collections::VecDeque;
13
14use crate::core::graph::EdgeId;
15use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
16
17pub fn fundamental_cycles(
59 graph: &Graph,
60 start_vid: Option<VertexId>,
61 bfs_cutoff: Option<u32>,
62) -> IgraphResult<Vec<Vec<EdgeId>>> {
63 let n = graph.vcount();
64
65 if let Some(v) = start_vid {
66 if v >= n {
67 return Err(IgraphError::InvalidArgument(format!(
68 "start vertex {v} out of range (vcount = {n})"
69 )));
70 }
71 }
72
73 let adj = build_incident_all(graph)?;
74
75 let mut visited: Vec<u8> = vec![0; n as usize];
76 let mut result: Vec<Vec<EdgeId>> = Vec::new();
77
78 if let Some(v) = start_vid {
79 fundamental_cycles_bfs(graph, &adj, &mut result, v, bfs_cutoff, &mut visited, 0)?;
80 } else {
81 for v in 0..n {
82 if visited[v as usize] == 0 {
83 fundamental_cycles_bfs(graph, &adj, &mut result, v, bfs_cutoff, &mut visited, 0)?;
84 }
85 }
86 }
87
88 Ok(result)
89}
90
91fn build_incident_all(graph: &Graph) -> IgraphResult<Vec<Vec<(EdgeId, VertexId)>>> {
92 let n = graph.vcount() as usize;
93 let mut adj: Vec<Vec<(EdgeId, VertexId)>> = vec![Vec::new(); n];
94
95 for eid in 0..graph.ecount() {
96 #[allow(clippy::cast_possible_truncation)]
97 let eid32 = eid as EdgeId;
98 let (from, to) = graph.edge(eid32)?;
99 if from == to {
100 adj[from as usize].push((eid32, to));
101 } else {
102 adj[from as usize].push((eid32, to));
103 adj[to as usize].push((eid32, from));
104 }
105 }
106
107 Ok(adj)
108}
109
110fn fundamental_cycles_bfs(
111 graph: &Graph,
112 adj: &[Vec<(EdgeId, VertexId)>],
113 result: &mut Vec<Vec<EdgeId>>,
114 start: VertexId,
115 bfs_cutoff: Option<u32>,
116 visited: &mut [u8],
117 _mark: u8,
118) -> IgraphResult<()> {
119 let n = graph.vcount() as usize;
120 let mut pred_edge: Vec<i64> = vec![-1; n];
121 let mut queue: VecDeque<(VertexId, u32)> = VecDeque::new();
122
123 visited[start as usize] = 1; queue.push_back((start, 0));
125
126 while let Some((v, vdist)) = queue.pop_front() {
127 for &(eid, u) in &adj[v as usize] {
128 if i64::from(eid) == pred_edge[v as usize] {
129 continue;
130 }
131
132 let u_state = visited[u as usize];
133
134 if u_state == 2 {
135 continue;
136 }
137 if u_state == 1 {
138 let cycle = trace_cycle(graph, pred_edge.as_slice(), eid, u, v)?;
139 result.push(cycle);
140 } else if bfs_cutoff.is_none() || vdist < bfs_cutoff.unwrap_or(0) {
141 visited[u as usize] = 1;
142 pred_edge[u as usize] = i64::from(eid);
143 queue.push_back((u, vdist.saturating_add(1)));
144 }
145 }
146
147 visited[v as usize] = 2; }
149
150 Ok(())
151}
152
153fn trace_cycle(
154 graph: &Graph,
155 pred_edge: &[i64],
156 closing_edge: EdgeId,
157 u: VertexId,
158 v: VertexId,
159) -> IgraphResult<Vec<EdgeId>> {
160 let mut u_back: Vec<EdgeId> = Vec::new();
161 let mut v_back: Vec<EdgeId> = vec![closing_edge];
162
163 let mut up = u;
164 let mut vp = v;
165
166 loop {
167 if up == vp {
168 break;
169 }
170
171 let u_pred = pred_edge[up as usize];
172 if u_pred < 0 {
173 break;
174 }
175 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
176 let u_eid = u_pred as EdgeId;
177 u_back.push(u_eid);
178 up = graph.edge_other(u_eid, up)?;
179
180 if up == vp {
181 break;
182 }
183
184 let v_pred = pred_edge[vp as usize];
185 if v_pred < 0 {
186 break;
187 }
188 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
189 let v_eid = v_pred as EdgeId;
190 v_back.push(v_eid);
191 vp = graph.edge_other(v_eid, vp)?;
192 }
193
194 let mut cycle = v_back;
196 cycle.extend(u_back.into_iter().rev());
197 Ok(cycle)
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn empty_graph() {
206 let g = Graph::with_vertices(0);
207 let cycles = fundamental_cycles(&g, None, None).unwrap();
208 assert!(cycles.is_empty());
209 }
210
211 #[test]
212 fn tree_no_cycles() {
213 let mut g = Graph::with_vertices(5);
214 g.add_edge(0, 1).unwrap();
215 g.add_edge(0, 2).unwrap();
216 g.add_edge(1, 3).unwrap();
217 g.add_edge(1, 4).unwrap();
218 let cycles = fundamental_cycles(&g, None, None).unwrap();
219 assert!(cycles.is_empty());
220 }
221
222 #[test]
223 fn single_triangle() {
224 let mut g = Graph::with_vertices(3);
225 g.add_edge(0, 1).unwrap();
226 g.add_edge(1, 2).unwrap();
227 g.add_edge(2, 0).unwrap();
228 let cycles = fundamental_cycles(&g, None, None).unwrap();
229 assert_eq!(cycles.len(), 1);
230 assert_eq!(cycles[0].len(), 3);
231 }
232
233 #[test]
234 fn k4_three_cycles() {
235 let mut g = Graph::with_vertices(4);
236 g.add_edge(0, 1).unwrap(); g.add_edge(0, 2).unwrap(); g.add_edge(0, 3).unwrap(); g.add_edge(1, 2).unwrap(); g.add_edge(1, 3).unwrap(); g.add_edge(2, 3).unwrap(); let cycles = fundamental_cycles(&g, None, None).unwrap();
243 assert_eq!(cycles.len(), 3);
245 }
246
247 #[test]
248 fn two_components() {
249 let mut g = Graph::with_vertices(6);
250 g.add_edge(0, 1).unwrap();
252 g.add_edge(1, 2).unwrap();
253 g.add_edge(2, 0).unwrap();
254 g.add_edge(3, 4).unwrap();
256 g.add_edge(4, 5).unwrap();
257 g.add_edge(5, 3).unwrap();
258 let cycles = fundamental_cycles(&g, None, None).unwrap();
259 assert_eq!(cycles.len(), 2);
260 }
261
262 #[test]
263 fn start_vertex_single_component() {
264 let mut g = Graph::with_vertices(6);
265 g.add_edge(0, 1).unwrap();
267 g.add_edge(1, 2).unwrap();
268 g.add_edge(2, 0).unwrap();
269 g.add_edge(3, 4).unwrap();
271 g.add_edge(4, 5).unwrap();
272 g.add_edge(5, 3).unwrap();
273 let cycles = fundamental_cycles(&g, Some(0), None).unwrap();
274 assert_eq!(cycles.len(), 1);
275 }
276
277 #[test]
278 fn start_vertex_out_of_range() {
279 let g = Graph::with_vertices(3);
280 assert!(fundamental_cycles(&g, Some(5), None).is_err());
281 }
282
283 #[test]
284 fn cycle_rank_formula() {
285 let mut g = Graph::with_vertices(5);
287 g.add_edge(0, 1).unwrap();
288 g.add_edge(1, 2).unwrap();
289 g.add_edge(2, 3).unwrap();
290 g.add_edge(3, 4).unwrap();
291 g.add_edge(4, 0).unwrap(); g.add_edge(0, 2).unwrap(); let cycles = fundamental_cycles(&g, None, None).unwrap();
294 assert_eq!(cycles.len(), 2);
296 }
297
298 #[test]
299 fn self_loop() {
300 let mut g = Graph::with_vertices(2);
301 g.add_edge(0, 0).unwrap(); g.add_edge(0, 1).unwrap();
303 let cycles = fundamental_cycles(&g, None, None).unwrap();
304 assert_eq!(cycles.len(), 1);
305 assert_eq!(cycles[0].len(), 1); }
307
308 #[test]
309 fn multi_edge() {
310 let mut g = Graph::with_vertices(2);
311 g.add_edge(0, 1).unwrap();
312 g.add_edge(0, 1).unwrap(); let cycles = fundamental_cycles(&g, None, None).unwrap();
314 assert_eq!(cycles.len(), 1);
315 assert_eq!(cycles[0].len(), 2); }
317
318 #[test]
319 fn directed_graph_treated_as_undirected() {
320 let mut g = Graph::new(3, true).unwrap();
321 g.add_edge(0, 1).unwrap();
322 g.add_edge(1, 2).unwrap();
323 g.add_edge(2, 0).unwrap();
324 let cycles = fundamental_cycles(&g, None, None).unwrap();
325 assert_eq!(cycles.len(), 1);
326 }
327
328 #[test]
329 fn bfs_cutoff_limits_cycles() {
330 let mut g = Graph::with_vertices(5);
332 g.add_edge(0, 1).unwrap();
333 g.add_edge(1, 2).unwrap();
334 g.add_edge(2, 3).unwrap();
335 g.add_edge(3, 4).unwrap();
336 g.add_edge(4, 0).unwrap(); g.add_edge(0, 2).unwrap(); let cycles = fundamental_cycles(&g, None, Some(1)).unwrap();
341 for cycle in &cycles {
342 assert!(cycle.len() <= 3);
343 }
344 }
345
346 #[test]
347 fn isolated_vertices() {
348 let g = Graph::with_vertices(10);
349 let cycles = fundamental_cycles(&g, None, None).unwrap();
350 assert!(cycles.is_empty());
351 }
352
353 #[test]
354 fn single_vertex() {
355 let g = Graph::with_vertices(1);
356 let cycles = fundamental_cycles(&g, None, None).unwrap();
357 assert!(cycles.is_empty());
358 }
359}