Skip to main content

rust_igraph/algorithms/
simple_cycles.rs

1//! Enumerate all simple cycles — Johnson's algorithm (ALGO-CY-003).
2//!
3//! Counterpart of `igraph_simple_cycles()` from
4//! `references/igraph/src/cycles/simple_cycles.c`.
5//!
6//! A simple cycle is a closed path without repeated vertices.
7//! Uses Johnson's algorithm (1975) which systematically enumerates
8//! all elementary circuits of a directed graph. For undirected graphs,
9//! the algorithm treats them as symmetric directed graphs with
10//! duplicate-cycle suppression.
11//!
12//! Reference: Johnson DB, "Finding all the elementary circuits of a
13//! directed graph." SIAM J. Comput. 4(1):77–84, 1975.
14
15use crate::core::graph::EdgeId;
16use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
17
18/// Mode for following edges during cycle search.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum SimpleCycleMode {
21    /// Follow outgoing edges (directed graphs only).
22    Out,
23    /// Follow incoming edges (directed graphs only).
24    In,
25    /// Ignore edge directions.
26    All,
27}
28
29/// A single simple cycle found by Johnson's algorithm.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct SimpleCycle {
32    /// Vertex IDs forming the cycle (ordered, not repeated).
33    pub vertices: Vec<VertexId>,
34    /// Edge IDs forming the cycle (ordered).
35    pub edges: Vec<EdgeId>,
36}
37
38/// Finds all simple cycles in the graph using Johnson's algorithm.
39///
40/// Returns cycles with length in `[min_length, max_length]`.
41/// If `max_results` is `Some(n)`, at most `n` cycles are returned.
42///
43/// For undirected graphs, `mode` is ignored. Each undirected cycle is
44/// reported once (the canonical orientation has the first edge id
45/// smaller than the closing edge id).
46///
47/// # Errors
48///
49/// Returns [`IgraphError::InvalidArgument`] if `min_length < 1`.
50///
51/// # Examples
52///
53/// ```
54/// use rust_igraph::{Graph, simple_cycles, SimpleCycleMode};
55///
56/// // Directed triangle: 0→1→2→0
57/// let mut g = Graph::new(3, true).unwrap();
58/// g.add_edge(0, 1).unwrap();
59/// g.add_edge(1, 2).unwrap();
60/// g.add_edge(2, 0).unwrap();
61///
62/// let cycles = simple_cycles(&g, SimpleCycleMode::Out, 1, None, None).unwrap();
63/// assert_eq!(cycles.len(), 1);
64/// assert_eq!(cycles[0].vertices, vec![0, 1, 2]);
65/// assert_eq!(cycles[0].edges, vec![0, 1, 2]);
66/// ```
67pub fn simple_cycles(
68    graph: &Graph,
69    mode: SimpleCycleMode,
70    min_length: u32,
71    max_length: Option<u32>,
72    max_results: Option<usize>,
73) -> IgraphResult<Vec<SimpleCycle>> {
74    if min_length < 1 {
75        return Err(IgraphError::InvalidArgument(
76            "simple_cycles: min_length must be >= 1".to_string(),
77        ));
78    }
79
80    let n = graph.vcount();
81    if n == 0 {
82        return Ok(Vec::new());
83    }
84
85    let max_len = max_length.unwrap_or(n);
86    if max_len == 0 {
87        return Ok(Vec::new());
88    }
89
90    let directed = graph.is_directed() && mode != SimpleCycleMode::All;
91
92    // Build adjacency + incidence lists according to mode.
93    let (adj, inc) = build_adj_inc(graph, mode)?;
94
95    // Mutable copies that get progressively shrunk as vertices are processed.
96    let n_usize = n as usize;
97    let mut adj_work: Vec<Vec<VertexId>> = adj.clone();
98    let mut inc_work: Vec<Vec<EdgeId>> = inc.clone();
99
100    let mut blocked: Vec<bool> = vec![false; n_usize];
101    let mut b_lists: Vec<Vec<VertexId>> = vec![Vec::new(); n_usize];
102
103    let mut vertex_stack: Vec<VertexId> = Vec::new();
104    let mut edge_stack: Vec<EdgeId> = Vec::new();
105
106    let mut results: Vec<SimpleCycle> = Vec::new();
107    let mut stop = false;
108
109    for s in 0..n {
110        if stop {
111            break;
112        }
113
114        // Reset blocked and B for vertices >= s.
115        for i in (s as usize)..n_usize {
116            blocked[i] = false;
117            b_lists[i].clear();
118        }
119
120        // Skip vertices that can't start a cycle (degree < 2 in undirected sense,
121        // and already visited in a prior component).
122        if adj_work[s as usize].is_empty() {
123            continue;
124        }
125
126        // Search for cycles starting from s.
127        johnson_circuit(
128            &adj_work,
129            &inc_work,
130            s,
131            s,
132            directed,
133            min_length,
134            max_len,
135            max_results,
136            &mut blocked,
137            &mut b_lists,
138            &mut vertex_stack,
139            &mut edge_stack,
140            &mut results,
141            &mut stop,
142        );
143
144        // Remove vertex s from the graph (Johnson's L3 step).
145        for i in 0..n_usize {
146            if let Some(pos) = adj_work[i].iter().position(|&v| v == s) {
147                adj_work[i].remove(pos);
148                inc_work[i].remove(pos);
149            }
150        }
151        adj_work[s as usize].clear();
152        inc_work[s as usize].clear();
153    }
154
155    Ok(results)
156}
157
158/// Adjacency and incidence lists (parallel vectors).
159type AdjInc = (Vec<Vec<VertexId>>, Vec<Vec<EdgeId>>);
160
161/// Build sorted adjacency and incidence lists for the given mode.
162fn build_adj_inc(graph: &Graph, mode: SimpleCycleMode) -> IgraphResult<AdjInc> {
163    let n = graph.vcount() as usize;
164    let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n];
165    let mut inc: Vec<Vec<EdgeId>> = vec![Vec::new(); n];
166
167    let m = graph.ecount();
168    for eid in 0..m {
169        let eid32 = u32::try_from(eid)
170            .map_err(|_| IgraphError::InvalidArgument("edge count exceeds u32".to_string()))?;
171        let (from, to) = graph.edge(eid32)?;
172
173        if graph.is_directed() {
174            match mode {
175                SimpleCycleMode::Out => {
176                    adj[from as usize].push(to);
177                    inc[from as usize].push(eid32);
178                }
179                SimpleCycleMode::In => {
180                    adj[to as usize].push(from);
181                    inc[to as usize].push(eid32);
182                }
183                SimpleCycleMode::All => {
184                    adj[from as usize].push(to);
185                    inc[from as usize].push(eid32);
186                    adj[to as usize].push(from);
187                    inc[to as usize].push(eid32);
188                }
189            }
190        } else {
191            adj[from as usize].push(to);
192            inc[from as usize].push(eid32);
193            adj[to as usize].push(from);
194            inc[to as usize].push(eid32);
195        }
196    }
197
198    // Sort each adjacency list by neighbor id (keep inc in sync).
199    for v in 0..n {
200        let mut pairs: Vec<(VertexId, EdgeId)> =
201            adj[v].iter().copied().zip(inc[v].iter().copied()).collect();
202        pairs.sort_unstable();
203        adj[v] = pairs.iter().map(|&(a, _)| a).collect();
204        inc[v] = pairs.iter().map(|&(_, e)| e).collect();
205    }
206
207    Ok((adj, inc))
208}
209
210/// Johnson's CIRCUIT procedure (iterative).
211#[allow(clippy::too_many_arguments)]
212fn johnson_circuit(
213    adj: &[Vec<VertexId>],
214    inc: &[Vec<EdgeId>],
215    s: VertexId,
216    start_v: VertexId,
217    directed: bool,
218    min_length: u32,
219    max_length: u32,
220    max_results: Option<usize>,
221    blocked: &mut [bool],
222    b_lists: &mut [Vec<VertexId>],
223    vertex_stack: &mut Vec<VertexId>,
224    edge_stack: &mut Vec<EdgeId>,
225    results: &mut Vec<SimpleCycle>,
226    stop: &mut bool,
227) {
228    // Iterative DFS using an explicit frame stack.
229    // Each frame tracks: which vertex we're at, where we are in its neighbor list,
230    // and whether we found a cycle below.
231    let mut frame_v: Vec<VertexId> = Vec::new();
232    let mut frame_idx: Vec<usize> = Vec::new();
233    let mut frame_found: Vec<bool> = Vec::new();
234    let mut frame_length_stop: Vec<bool> = Vec::new();
235
236    // Push the initial frame for the start vertex.
237    vertex_stack.push(start_v);
238    blocked[start_v as usize] = true;
239    frame_v.push(start_v);
240    frame_idx.push(0);
241    frame_found.push(false);
242    frame_length_stop.push(false);
243
244    loop {
245        if *stop || frame_v.is_empty() {
246            break;
247        }
248
249        let depth = frame_v.len() - 1;
250        let v = frame_v[depth];
251        let v_idx = v as usize;
252        let neighbors = &adj[v_idx];
253        let incidents = &inc[v_idx];
254
255        let mut advanced = false;
256
257        while frame_idx[depth] < neighbors.len() {
258            let i = frame_idx[depth];
259            frame_idx[depth] += 1;
260
261            let w = neighbors[i];
262            let we = incidents[i];
263
264            if w == s {
265                frame_found[depth] = true;
266
267                // For undirected graphs, suppress duplicates.
268                if !directed {
269                    if edge_stack.len() == 1 && edge_stack[0] == we {
270                        continue;
271                    }
272                    if !edge_stack.is_empty() && edge_stack[0] > we {
273                        continue;
274                    }
275                }
276
277                let cycle_len = vertex_stack.len();
278                let min_l = min_length as usize;
279                let max_l = max_length as usize;
280                if cycle_len >= min_l && cycle_len <= max_l {
281                    let mut cycle_edges = edge_stack.clone();
282                    cycle_edges.push(we);
283                    results.push(SimpleCycle {
284                        vertices: vertex_stack.clone(),
285                        edges: cycle_edges,
286                    });
287
288                    if let Some(max) = max_results {
289                        if results.len() >= max {
290                            *stop = true;
291                            break;
292                        }
293                    }
294                }
295            } else if !blocked[w as usize] {
296                let can_recurse = vertex_stack.len() < max_length as usize;
297                if can_recurse {
298                    vertex_stack.push(w);
299                    edge_stack.push(we);
300                    blocked[w as usize] = true;
301
302                    frame_v.push(w);
303                    frame_idx.push(0);
304                    frame_found.push(false);
305                    frame_length_stop.push(false);
306                    advanced = true;
307                    break;
308                }
309                frame_length_stop[depth] = true;
310            }
311        }
312
313        if *stop {
314            break;
315        }
316
317        if !advanced {
318            let found = frame_found[depth];
319            let length_stop = frame_length_stop[depth];
320            let cur_v = frame_v[depth];
321
322            frame_v.pop();
323            frame_idx.pop();
324            frame_found.pop();
325            frame_length_stop.pop();
326
327            if found || length_stop {
328                unblock(cur_v, blocked, b_lists);
329            } else {
330                for &w in &adj[cur_v as usize] {
331                    let w_b = &mut b_lists[w as usize];
332                    if !w_b.contains(&cur_v) {
333                        w_b.push(cur_v);
334                    }
335                }
336            }
337
338            vertex_stack.pop();
339            edge_stack.pop();
340
341            // Propagate "found" to parent frame.
342            if !frame_v.is_empty() && (found || length_stop) {
343                let parent = frame_v.len() - 1;
344                frame_found[parent] = true;
345            }
346        }
347    }
348
349    vertex_stack.clear();
350    edge_stack.clear();
351}
352
353/// Johnson's UNBLOCK procedure (iterative).
354fn unblock(u: VertexId, blocked: &mut [bool], b_lists: &mut [Vec<VertexId>]) {
355    let mut stack: Vec<VertexId> = vec![u];
356
357    while let Some(v) = stack.pop() {
358        if blocked[v as usize] {
359            blocked[v as usize] = false;
360            while let Some(w) = b_lists[v as usize].pop() {
361                if blocked[w as usize] {
362                    stack.push(w);
363                }
364            }
365        }
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372
373    #[test]
374    fn empty_graph() {
375        let g = Graph::with_vertices(0);
376        let cycles = simple_cycles(&g, SimpleCycleMode::Out, 1, None, None).unwrap();
377        assert!(cycles.is_empty());
378    }
379
380    #[test]
381    fn no_edges() {
382        let g = Graph::with_vertices(5);
383        let cycles = simple_cycles(&g, SimpleCycleMode::Out, 1, None, None).unwrap();
384        assert!(cycles.is_empty());
385    }
386
387    #[test]
388    fn directed_triangle() {
389        let mut g = Graph::new(3, true).unwrap();
390        g.add_edge(0, 1).unwrap();
391        g.add_edge(1, 2).unwrap();
392        g.add_edge(2, 0).unwrap();
393        let cycles = simple_cycles(&g, SimpleCycleMode::Out, 1, None, None).unwrap();
394        assert_eq!(cycles.len(), 1);
395        assert_eq!(cycles[0].vertices, vec![0, 1, 2]);
396        assert_eq!(cycles[0].edges, vec![0, 1, 2]);
397    }
398
399    #[test]
400    fn directed_two_cycles() {
401        // 0→1→0 and 0→1→2→0
402        let mut g = Graph::new(3, true).unwrap();
403        g.add_edge(0, 1).unwrap(); // 0
404        g.add_edge(1, 0).unwrap(); // 1
405        g.add_edge(1, 2).unwrap(); // 2
406        g.add_edge(2, 0).unwrap(); // 3
407        let cycles = simple_cycles(&g, SimpleCycleMode::Out, 1, None, None).unwrap();
408        assert_eq!(cycles.len(), 2);
409    }
410
411    #[test]
412    fn undirected_triangle() {
413        let mut g = Graph::with_vertices(3);
414        g.add_edge(0, 1).unwrap();
415        g.add_edge(1, 2).unwrap();
416        g.add_edge(0, 2).unwrap();
417        let cycles = simple_cycles(&g, SimpleCycleMode::All, 1, None, None).unwrap();
418        // One triangle
419        assert_eq!(cycles.len(), 1);
420        assert_eq!(cycles[0].vertices.len(), 3);
421    }
422
423    #[test]
424    fn undirected_square() {
425        // 0-1-2-3-0: one 4-cycle
426        let mut g = Graph::with_vertices(4);
427        g.add_edge(0, 1).unwrap();
428        g.add_edge(1, 2).unwrap();
429        g.add_edge(2, 3).unwrap();
430        g.add_edge(3, 0).unwrap();
431        let cycles = simple_cycles(&g, SimpleCycleMode::All, 1, None, None).unwrap();
432        assert_eq!(cycles.len(), 1);
433        assert_eq!(cycles[0].vertices.len(), 4);
434    }
435
436    #[test]
437    fn undirected_complete_4() {
438        // K4 has 7 simple cycles: 4 triangles + 3 four-cycles
439        let mut g = Graph::with_vertices(4);
440        g.add_edge(0, 1).unwrap();
441        g.add_edge(0, 2).unwrap();
442        g.add_edge(0, 3).unwrap();
443        g.add_edge(1, 2).unwrap();
444        g.add_edge(1, 3).unwrap();
445        g.add_edge(2, 3).unwrap();
446        let cycles = simple_cycles(&g, SimpleCycleMode::All, 1, None, None).unwrap();
447        assert_eq!(cycles.len(), 7);
448    }
449
450    #[test]
451    fn min_length_filter() {
452        // K4: filter out triangles (length 3), only keep 4-cycles
453        let mut g = Graph::with_vertices(4);
454        g.add_edge(0, 1).unwrap();
455        g.add_edge(0, 2).unwrap();
456        g.add_edge(0, 3).unwrap();
457        g.add_edge(1, 2).unwrap();
458        g.add_edge(1, 3).unwrap();
459        g.add_edge(2, 3).unwrap();
460        let cycles = simple_cycles(&g, SimpleCycleMode::All, 4, None, None).unwrap();
461        assert_eq!(cycles.len(), 3);
462        for c in &cycles {
463            assert_eq!(c.vertices.len(), 4);
464        }
465    }
466
467    #[test]
468    fn max_length_filter() {
469        // K4: only triangles (max_length=3)
470        let mut g = Graph::with_vertices(4);
471        g.add_edge(0, 1).unwrap();
472        g.add_edge(0, 2).unwrap();
473        g.add_edge(0, 3).unwrap();
474        g.add_edge(1, 2).unwrap();
475        g.add_edge(1, 3).unwrap();
476        g.add_edge(2, 3).unwrap();
477        let cycles = simple_cycles(&g, SimpleCycleMode::All, 1, Some(3), None).unwrap();
478        assert_eq!(cycles.len(), 4);
479        for c in &cycles {
480            assert_eq!(c.vertices.len(), 3);
481        }
482    }
483
484    #[test]
485    fn max_results() {
486        // K4 has 7 cycles, limit to 3
487        let mut g = Graph::with_vertices(4);
488        g.add_edge(0, 1).unwrap();
489        g.add_edge(0, 2).unwrap();
490        g.add_edge(0, 3).unwrap();
491        g.add_edge(1, 2).unwrap();
492        g.add_edge(1, 3).unwrap();
493        g.add_edge(2, 3).unwrap();
494        let cycles = simple_cycles(&g, SimpleCycleMode::All, 1, None, Some(3)).unwrap();
495        assert_eq!(cycles.len(), 3);
496    }
497
498    #[test]
499    fn self_loop_directed() {
500        let mut g = Graph::new(2, true).unwrap();
501        g.add_edge(0, 0).unwrap(); // self-loop
502        g.add_edge(0, 1).unwrap();
503        let cycles = simple_cycles(&g, SimpleCycleMode::Out, 1, None, None).unwrap();
504        assert_eq!(cycles.len(), 1);
505        assert_eq!(cycles[0].vertices, vec![0]);
506        assert_eq!(cycles[0].edges, vec![0]);
507    }
508
509    #[test]
510    fn directed_in_mode() {
511        // 0→1→2→0: in IN mode, the reversed direction has a cycle 0←2←1←0
512        let mut g = Graph::new(3, true).unwrap();
513        g.add_edge(0, 1).unwrap();
514        g.add_edge(1, 2).unwrap();
515        g.add_edge(2, 0).unwrap();
516        let cycles = simple_cycles(&g, SimpleCycleMode::In, 1, None, None).unwrap();
517        assert_eq!(cycles.len(), 1);
518        // In reverse: 0←2←1←0, so vertices should be [0, 2, 1]
519        assert_eq!(cycles[0].vertices.len(), 3);
520    }
521
522    #[test]
523    fn dag_no_cycles() {
524        let mut g = Graph::new(4, true).unwrap();
525        g.add_edge(0, 1).unwrap();
526        g.add_edge(0, 2).unwrap();
527        g.add_edge(1, 3).unwrap();
528        g.add_edge(2, 3).unwrap();
529        let cycles = simple_cycles(&g, SimpleCycleMode::Out, 1, None, None).unwrap();
530        assert!(cycles.is_empty());
531    }
532
533    #[test]
534    fn tree_no_cycles() {
535        let mut g = Graph::with_vertices(5);
536        g.add_edge(0, 1).unwrap();
537        g.add_edge(0, 2).unwrap();
538        g.add_edge(1, 3).unwrap();
539        g.add_edge(1, 4).unwrap();
540        let cycles = simple_cycles(&g, SimpleCycleMode::All, 1, None, None).unwrap();
541        assert!(cycles.is_empty());
542    }
543
544    #[test]
545    fn directed_k3_all_edges() {
546        // Complete directed graph on 3 vertices: 6 directed edges, 8 cycles
547        // (3 two-cycles + 2 three-cycles... actually: 3 pairs of opposite edges =
548        //  3 two-cycles, plus 2 directed triangles = 5 total? Let's count.)
549        // Actually K3 directed has edges: 0→1, 1→0, 0→2, 2→0, 1→2, 2→1
550        // Two-cycles: {0,1}, {0,2}, {1,2} = 3
551        // Three-cycles: 0→1→2→0 and 0→2→1→0 = 2
552        // Total = 5
553        let mut g = Graph::new(3, true).unwrap();
554        g.add_edge(0, 1).unwrap();
555        g.add_edge(1, 0).unwrap();
556        g.add_edge(0, 2).unwrap();
557        g.add_edge(2, 0).unwrap();
558        g.add_edge(1, 2).unwrap();
559        g.add_edge(2, 1).unwrap();
560        let cycles = simple_cycles(&g, SimpleCycleMode::Out, 1, None, None).unwrap();
561        assert_eq!(cycles.len(), 5);
562    }
563}