1use std::collections::VecDeque;
15
16use crate::algorithms::connectivity::components::connected_components;
17use crate::core::graph::EdgeId;
18use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
19
20pub fn minimum_cycle_basis(
72 graph: &Graph,
73 bfs_cutoff: Option<u32>,
74 complete: bool,
75) -> IgraphResult<Vec<Vec<EdgeId>>> {
76 let n = graph.vcount();
77 let ecount = graph.ecount();
78
79 if n == 0 || ecount == 0 {
80 return Ok(Vec::new());
81 }
82
83 let adj = build_incident_all(graph)?;
84
85 let cc = connected_components(graph)?;
86 let num_components = cc.count;
87
88 #[allow(clippy::cast_possible_truncation)]
89 let rank = (ecount as u32)
90 .saturating_sub(n)
91 .saturating_add(num_components);
92
93 if rank == 0 {
94 return Ok(Vec::new());
95 }
96
97 let degrees = compute_degrees(&adj, n);
98
99 let mut visited: Vec<u32> = vec![0; n as usize];
100 let mut candidates: Vec<Vec<EdgeId>> = Vec::new();
101 let mut mark: u32 = 0;
102
103 for i in 0..n {
104 let deg = degrees[i as usize];
105 let vis = visited[i as usize] % 3 != 0;
106
107 if deg <= 1 || (vis && deg < 3) {
108 continue;
109 }
110
111 let cutoff = if vis || !complete { bfs_cutoff } else { None };
112
113 fundamental_cycles_bfs_horton(graph, &adj, &mut candidates, i, cutoff, &mut visited, mark)?;
114 mark = mark.checked_add(3).ok_or_else(|| {
115 IgraphError::InvalidArgument("mark overflow in minimum_cycle_basis".into())
116 })?;
117 }
118
119 for c in &mut candidates {
121 c.sort_unstable();
122 }
123 candidates.sort_unstable_by(cycle_cmp);
124 candidates.dedup();
125
126 let mut result: Vec<Vec<EdgeId>> = Vec::with_capacity(rank as usize);
128 let mut reduced_matrix: Vec<Vec<EdgeId>> = Vec::new();
129
130 for cycle in &candidates {
131 let independent = gaussian_elimination_check(&mut reduced_matrix, cycle);
132 if independent {
133 result.push(cycle.clone());
134 }
135 if reduced_matrix.len() == rank as usize {
136 break;
137 }
138 }
139
140 Ok(result)
141}
142
143fn build_incident_all(graph: &Graph) -> IgraphResult<Vec<Vec<(EdgeId, VertexId)>>> {
144 let n = graph.vcount() as usize;
145 let mut adj: Vec<Vec<(EdgeId, VertexId)>> = vec![Vec::new(); n];
146
147 for eid in 0..graph.ecount() {
148 #[allow(clippy::cast_possible_truncation)]
149 let eid32 = eid as EdgeId;
150 let (from, to) = graph.edge(eid32)?;
151 if from == to {
152 adj[from as usize].push((eid32, to));
153 } else {
154 adj[from as usize].push((eid32, to));
155 adj[to as usize].push((eid32, from));
156 }
157 }
158
159 Ok(adj)
160}
161
162fn compute_degrees(adj: &[Vec<(EdgeId, VertexId)>], n: u32) -> Vec<u32> {
163 let mut degrees = vec![0u32; n as usize];
164 for (i, neighbors) in adj.iter().enumerate() {
165 #[allow(clippy::cast_possible_truncation)]
166 {
167 degrees[i] = neighbors.len() as u32;
168 }
169 }
170 degrees
171}
172
173fn fundamental_cycles_bfs_horton(
174 graph: &Graph,
175 adj: &[Vec<(EdgeId, VertexId)>],
176 result: &mut Vec<Vec<EdgeId>>,
177 start: VertexId,
178 bfs_cutoff: Option<u32>,
179 visited: &mut [u32],
180 mark: u32,
181) -> IgraphResult<()> {
182 let n = graph.vcount() as usize;
183 let mut pred_edge: Vec<i64> = vec![-1; n];
184 let mut queue: VecDeque<(VertexId, u32)> = VecDeque::new();
185
186 visited[start as usize] = mark + 1;
187 queue.push_back((start, 0));
188
189 while let Some((v, vdist)) = queue.pop_front() {
190 for &(eid, u) in &adj[v as usize] {
191 if i64::from(eid) == pred_edge[v as usize] {
192 continue;
193 }
194
195 let u_state = visited[u as usize];
196
197 if u_state == mark + 2 {
198 continue;
199 }
200 if u_state == mark + 1 {
201 let cycle = trace_cycle(graph, pred_edge.as_slice(), eid, u, v)?;
202 result.push(cycle);
203 } else if bfs_cutoff.is_none() || vdist < bfs_cutoff.unwrap_or(0) {
204 visited[u as usize] = mark + 1;
205 pred_edge[u as usize] = i64::from(eid);
206 queue.push_back((u, vdist.saturating_add(1)));
207 }
208 }
209
210 visited[v as usize] = mark + 2;
211 }
212
213 Ok(())
214}
215
216fn trace_cycle(
217 graph: &Graph,
218 pred_edge: &[i64],
219 closing_edge: EdgeId,
220 u: VertexId,
221 v: VertexId,
222) -> IgraphResult<Vec<EdgeId>> {
223 let mut u_back: Vec<EdgeId> = Vec::new();
224 let mut v_back: Vec<EdgeId> = vec![closing_edge];
225
226 let mut up = u;
227 let mut vp = v;
228
229 loop {
230 if up == vp {
231 break;
232 }
233
234 let u_pred = pred_edge[up as usize];
235 if u_pred < 0 {
236 break;
237 }
238 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
239 let u_eid = u_pred as EdgeId;
240 u_back.push(u_eid);
241 up = graph.edge_other(u_eid, up)?;
242
243 if up == vp {
244 break;
245 }
246
247 let v_pred = pred_edge[vp as usize];
248 if v_pred < 0 {
249 break;
250 }
251 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
252 let v_eid = v_pred as EdgeId;
253 v_back.push(v_eid);
254 vp = graph.edge_other(v_eid, vp)?;
255 }
256
257 let mut cycle = v_back;
258 cycle.extend(u_back.into_iter().rev());
259 Ok(cycle)
260}
261
262fn cycle_cmp(a: &Vec<EdgeId>, b: &Vec<EdgeId>) -> std::cmp::Ordering {
263 a.len().cmp(&b.len()).then_with(|| a.cmp(b))
264}
265
266fn cycle_add(a: &[EdgeId], b: &[EdgeId]) -> Vec<EdgeId> {
268 let mut result = Vec::with_capacity(a.len() + b.len());
269 let mut ia = 0;
270 let mut ib = 0;
271
272 while ia < a.len() && ib < b.len() {
273 match a[ia].cmp(&b[ib]) {
274 std::cmp::Ordering::Less => {
275 result.push(a[ia]);
276 ia += 1;
277 }
278 std::cmp::Ordering::Equal => {
279 ia += 1;
280 ib += 1;
281 }
282 std::cmp::Ordering::Greater => {
283 result.push(b[ib]);
284 ib += 1;
285 }
286 }
287 }
288
289 result.extend_from_slice(&a[ia..]);
290 result.extend_from_slice(&b[ib..]);
291 result
292}
293
294fn gaussian_elimination_check(reduced_matrix: &mut Vec<Vec<EdgeId>>, cycle: &[EdgeId]) -> bool {
297 let mut work: Vec<EdgeId> = cycle.to_vec();
298
299 let mut insert_pos = 0;
300 for (i, row) in reduced_matrix.iter().enumerate() {
301 if work.is_empty() {
302 return false;
303 }
304 match row[0].cmp(&work[0]) {
305 std::cmp::Ordering::Less => {
306 insert_pos = i + 1;
307 }
308 std::cmp::Ordering::Equal => {
309 work = cycle_add(row, &work);
310 if work.is_empty() {
311 return false;
312 }
313 insert_pos = i + 1;
314 }
315 std::cmp::Ordering::Greater => {
316 insert_pos = i;
317 break;
318 }
319 }
320 }
321
322 if work.is_empty() {
323 return false;
324 }
325
326 if insert_pos >= reduced_matrix.len() {
327 reduced_matrix.push(work);
328 } else {
329 reduced_matrix.insert(insert_pos, work);
330 }
331 true
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn empty_graph() {
340 let g = Graph::with_vertices(0);
341 let cycles = minimum_cycle_basis(&g, None, true).unwrap();
342 assert!(cycles.is_empty());
343 }
344
345 #[test]
346 fn tree_no_cycles() {
347 let mut g = Graph::with_vertices(5);
348 g.add_edge(0, 1).unwrap();
349 g.add_edge(0, 2).unwrap();
350 g.add_edge(1, 3).unwrap();
351 g.add_edge(1, 4).unwrap();
352 let cycles = minimum_cycle_basis(&g, None, true).unwrap();
353 assert!(cycles.is_empty());
354 }
355
356 #[test]
357 fn single_triangle() {
358 let mut g = Graph::with_vertices(3);
359 g.add_edge(0, 1).unwrap();
360 g.add_edge(1, 2).unwrap();
361 g.add_edge(2, 0).unwrap();
362 let cycles = minimum_cycle_basis(&g, None, true).unwrap();
363 assert_eq!(cycles.len(), 1);
364 assert_eq!(cycles[0].len(), 3);
365 }
366
367 #[test]
368 fn k4_all_triangles() {
369 let mut g = Graph::with_vertices(4);
370 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 = minimum_cycle_basis(&g, None, true).unwrap();
377 assert_eq!(cycles.len(), 3);
379 for c in &cycles {
380 assert_eq!(c.len(), 3);
381 }
382 }
383
384 #[test]
385 fn cycle_5_single_cycle() {
386 let mut g = Graph::with_vertices(5);
387 g.add_edge(0, 1).unwrap();
388 g.add_edge(1, 2).unwrap();
389 g.add_edge(2, 3).unwrap();
390 g.add_edge(3, 4).unwrap();
391 g.add_edge(4, 0).unwrap();
392 let cycles = minimum_cycle_basis(&g, None, true).unwrap();
393 assert_eq!(cycles.len(), 1);
395 assert_eq!(cycles[0].len(), 5);
396 }
397
398 #[test]
399 fn two_triangles_sharing_edge() {
400 let mut g = Graph::with_vertices(4);
402 g.add_edge(0, 1).unwrap(); g.add_edge(1, 2).unwrap(); g.add_edge(2, 0).unwrap(); g.add_edge(2, 3).unwrap(); g.add_edge(3, 1).unwrap(); let cycles = minimum_cycle_basis(&g, None, true).unwrap();
408 assert_eq!(cycles.len(), 2);
410 for c in &cycles {
411 assert_eq!(c.len(), 3);
412 }
413 }
414
415 #[test]
416 fn two_components() {
417 let mut g = Graph::with_vertices(6);
418 g.add_edge(0, 1).unwrap();
420 g.add_edge(1, 2).unwrap();
421 g.add_edge(2, 0).unwrap();
422 g.add_edge(3, 4).unwrap();
424 g.add_edge(4, 5).unwrap();
425 g.add_edge(5, 3).unwrap();
426 let cycles = minimum_cycle_basis(&g, None, true).unwrap();
427 assert_eq!(cycles.len(), 2);
429 }
430
431 #[test]
432 fn cycle_rank_formula() {
433 let mut g = Graph::with_vertices(5);
435 g.add_edge(0, 1).unwrap();
436 g.add_edge(1, 2).unwrap();
437 g.add_edge(2, 3).unwrap();
438 g.add_edge(3, 4).unwrap();
439 g.add_edge(4, 0).unwrap();
440 g.add_edge(0, 2).unwrap();
441 let cycles = minimum_cycle_basis(&g, None, true).unwrap();
442 assert_eq!(cycles.len(), 2);
444 }
445
446 #[test]
447 fn minimum_weight_property() {
448 let mut g = Graph::with_vertices(5);
452 g.add_edge(0, 1).unwrap(); g.add_edge(1, 2).unwrap(); g.add_edge(2, 0).unwrap(); g.add_edge(1, 3).unwrap(); g.add_edge(3, 4).unwrap(); g.add_edge(4, 0).unwrap(); let cycles = minimum_cycle_basis(&g, None, true).unwrap();
459 assert_eq!(cycles.len(), 2);
461 let total_weight: usize = cycles.iter().map(Vec::len).sum();
464 assert!(total_weight <= 8); }
466
467 #[test]
468 fn self_loop() {
469 let mut g = Graph::with_vertices(2);
470 g.add_edge(0, 0).unwrap(); g.add_edge(0, 1).unwrap();
472 let cycles = minimum_cycle_basis(&g, None, true).unwrap();
473 assert_eq!(cycles.len(), 1);
474 assert_eq!(cycles[0].len(), 1);
475 }
476
477 #[test]
478 fn multi_edge() {
479 let mut g = Graph::with_vertices(2);
480 g.add_edge(0, 1).unwrap();
481 g.add_edge(0, 1).unwrap();
482 let cycles = minimum_cycle_basis(&g, None, true).unwrap();
483 assert_eq!(cycles.len(), 1);
484 assert_eq!(cycles[0].len(), 2);
485 }
486
487 #[test]
488 fn directed_treated_as_undirected() {
489 let mut g = Graph::new(3, true).unwrap();
490 g.add_edge(0, 1).unwrap();
491 g.add_edge(1, 2).unwrap();
492 g.add_edge(2, 0).unwrap();
493 let cycles = minimum_cycle_basis(&g, None, true).unwrap();
494 assert_eq!(cycles.len(), 1);
495 }
496
497 #[test]
498 fn bfs_cutoff_complete() {
499 let mut g = Graph::with_vertices(4);
501 g.add_edge(0, 1).unwrap();
502 g.add_edge(0, 2).unwrap();
503 g.add_edge(0, 3).unwrap();
504 g.add_edge(1, 2).unwrap();
505 g.add_edge(1, 3).unwrap();
506 g.add_edge(2, 3).unwrap();
507 let cycles = minimum_cycle_basis(&g, Some(1), true).unwrap();
508 assert_eq!(cycles.len(), 3);
509 }
510
511 #[test]
512 fn isolated_vertices() {
513 let g = Graph::with_vertices(10);
514 let cycles = minimum_cycle_basis(&g, None, true).unwrap();
515 assert!(cycles.is_empty());
516 }
517
518 #[test]
519 fn single_vertex() {
520 let g = Graph::with_vertices(1);
521 let cycles = minimum_cycle_basis(&g, None, true).unwrap();
522 assert!(cycles.is_empty());
523 }
524
525 #[test]
526 fn petersen_graph_rank() {
527 let mut g = Graph::with_vertices(10);
530 g.add_edge(0, 1).unwrap();
532 g.add_edge(1, 2).unwrap();
533 g.add_edge(2, 3).unwrap();
534 g.add_edge(3, 4).unwrap();
535 g.add_edge(4, 0).unwrap();
536 g.add_edge(5, 7).unwrap();
538 g.add_edge(7, 9).unwrap();
539 g.add_edge(9, 6).unwrap();
540 g.add_edge(6, 8).unwrap();
541 g.add_edge(8, 5).unwrap();
542 g.add_edge(0, 5).unwrap();
544 g.add_edge(1, 6).unwrap();
545 g.add_edge(2, 7).unwrap();
546 g.add_edge(3, 8).unwrap();
547 g.add_edge(4, 9).unwrap();
548 let cycles = minimum_cycle_basis(&g, None, true).unwrap();
549 assert_eq!(cycles.len(), 6);
550 for c in &cycles {
552 assert_eq!(c.len(), 5);
553 }
554 }
555}