Skip to main content

rust_igraph/algorithms/properties/
girth.rs

1//! Girth — length of the shortest cycle (ALGO-PR-001).
2//!
3//! Counterpart of `igraph_girth()` from
4//! `references/igraph/src/properties/girth.c:73-220`.
5//!
6//! Algorithm: BFS from each vertex. When the BFS frontier touches an
7//! already-visited vertex, it has discovered a cycle whose length is
8//! `actlevel + neilevel - 1` (with the convention that the root is at
9//! level 1). The minimum across all starts is the girth.
10//!
11//! Implementation details that match upstream:
12//! - Self-loops and parallel edges are ignored (cycles of length 1 or 2
13//!   are not counted). We pre-build a *simple* neighbour list per vertex.
14//! - Once any cycle is found, BFS is capped at `stoplevel` so subsequent
15//!   starts can short-circuit. The first triangle (girth = 3) ends the
16//!   outer loop entirely (no shorter cycle exists in a simple graph).
17//! - Phase-1 minimal slice returns only the girth value (`Option<u32>`).
18//!   The cycle-vertex reconstruction (upstream's optional `cycle` output)
19//!   ships in PR-001b.
20
21use std::collections::VecDeque;
22
23use crate::core::{Graph, IgraphResult, VertexId};
24
25/// Shortest cycle length in `graph`. Returns `None` for acyclic graphs.
26///
27/// Self-loops and parallel edges are ignored — only "real" cycles of
28/// length ≥ 3 are considered. Counterpart of `igraph_girth(_, &girth, NULL)`
29/// where `IGRAPH_INFINITY` for acyclic maps to `None`.
30///
31/// Edge directions are ignored (`IGRAPH_ALL` semantics).
32///
33/// # Examples
34///
35/// ```
36/// use rust_igraph::{Graph, girth};
37///
38/// // Triangle: girth = 3.
39/// let mut g = Graph::with_vertices(3);
40/// g.add_edge(0, 1).unwrap();
41/// g.add_edge(1, 2).unwrap();
42/// g.add_edge(2, 0).unwrap();
43/// assert_eq!(girth(&g).unwrap(), Some(3));
44///
45/// // Tree: no cycles.
46/// let mut g = Graph::with_vertices(3);
47/// g.add_edge(0, 1).unwrap();
48/// g.add_edge(1, 2).unwrap();
49/// assert_eq!(girth(&g).unwrap(), None);
50/// ```
51pub fn girth(graph: &Graph) -> IgraphResult<Option<u32>> {
52    let n = graph.vcount();
53    if n < 3 {
54        return Ok(None);
55    }
56
57    // Pre-build simple (no-loop, no-multi) neighbour lists once.
58    // Equivalent to upstream's
59    // `igraph_lazy_adjlist_init(_, _, IGRAPH_ALL, NO_LOOPS, NO_MULTIPLE)`.
60    let n_us = n as usize;
61    let mut adj: Vec<Vec<VertexId>> = Vec::with_capacity(n_us);
62    for v in 0..n {
63        let raw = graph.neighbors(v)?;
64        let mut simple: Vec<VertexId> = raw.into_iter().filter(|&u| u != v).collect();
65        simple.sort_unstable();
66        simple.dedup();
67        adj.push(simple);
68    }
69
70    // upstream uses `IGRAPH_INTEGER_MAX` as the "no cycle yet" sentinel.
71    // We use `u32::MAX` for the same purpose.
72    let mut mincirc: u32 = u32::MAX;
73    let mut stoplevel: u32 = (n + 1).max(2);
74    let mut triangle = false;
75
76    let mut level: Vec<u32> = vec![0; n_us];
77    let mut q: VecDeque<VertexId> = VecDeque::new();
78
79    let mut node = 0u32;
80    while !triangle && node < n {
81        if node == 1 && mincirc == u32::MAX {
82            // Per upstream: if the graph is connected and we found no cycle
83            // starting from vertex 0, it's a tree → no cycles anywhere.
84            let cc = crate::algorithms::connectivity::connected_components(graph)?;
85            if cc.count == 1 {
86                break;
87            }
88        }
89
90        // Reset BFS state for this start vertex.
91        q.clear();
92        level.fill(0);
93        q.push_back(node);
94        level[node as usize] = 1;
95
96        while let Some(actnode) = q.pop_front() {
97            let actlevel = level[actnode as usize];
98            if actlevel >= stoplevel {
99                break;
100            }
101            let neilist = &adj[actnode as usize];
102            for &nei in neilist {
103                let neilevel = level[nei as usize];
104                if neilevel != 0 {
105                    if neilevel == actlevel - 1 {
106                        // Tree edge back to BFS parent — not a cycle.
107                        continue;
108                    }
109                    // Found a cycle of length `actlevel + neilevel - 1`.
110                    stoplevel = neilevel;
111                    let candidate = actlevel + neilevel - 1;
112                    if candidate < mincirc {
113                        mincirc = candidate;
114                        if neilevel == 2 {
115                            // Triangle — minimum possible in a simple graph.
116                            triangle = true;
117                        }
118                    }
119                    if neilevel == actlevel {
120                        break;
121                    }
122                } else {
123                    q.push_back(nei);
124                    level[nei as usize] = actlevel + 1;
125                }
126            }
127        }
128
129        node += 1;
130    }
131
132    if mincirc == u32::MAX {
133        Ok(None)
134    } else {
135        Ok(Some(mincirc))
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn empty_graph_has_no_girth() {
145        let g = Graph::with_vertices(0);
146        assert_eq!(girth(&g).unwrap(), None);
147    }
148
149    #[test]
150    fn singleton_has_no_girth() {
151        let g = Graph::with_vertices(1);
152        assert_eq!(girth(&g).unwrap(), None);
153    }
154
155    #[test]
156    fn isolated_vertices_have_no_girth() {
157        let g = Graph::with_vertices(5);
158        assert_eq!(girth(&g).unwrap(), None);
159    }
160
161    #[test]
162    fn tree_has_no_girth() {
163        // Path 0-1-2-3-4: a tree.
164        let mut g = Graph::with_vertices(5);
165        for i in 0..4 {
166            g.add_edge(i, i + 1).unwrap();
167        }
168        assert_eq!(girth(&g).unwrap(), None);
169    }
170
171    #[test]
172    fn triangle_girth_is_3() {
173        let mut g = Graph::with_vertices(3);
174        g.add_edge(0, 1).unwrap();
175        g.add_edge(1, 2).unwrap();
176        g.add_edge(2, 0).unwrap();
177        assert_eq!(girth(&g).unwrap(), Some(3));
178    }
179
180    #[test]
181    fn square_4_cycle_has_girth_4() {
182        let mut g = Graph::with_vertices(4);
183        for i in 0..4u32 {
184            g.add_edge(i, (i + 1) % 4).unwrap();
185        }
186        assert_eq!(girth(&g).unwrap(), Some(4));
187    }
188
189    #[test]
190    fn pentagon_has_girth_5() {
191        let mut g = Graph::with_vertices(5);
192        for i in 0..5u32 {
193            g.add_edge(i, (i + 1) % 5).unwrap();
194        }
195        assert_eq!(girth(&g).unwrap(), Some(5));
196    }
197
198    #[test]
199    fn k4_has_girth_3() {
200        let mut g = Graph::with_vertices(4);
201        for u in 0..4u32 {
202            for v in (u + 1)..4 {
203                g.add_edge(u, v).unwrap();
204            }
205        }
206        assert_eq!(girth(&g).unwrap(), Some(3));
207    }
208
209    #[test]
210    fn self_loop_does_not_count_as_cycle() {
211        // Self-loop on 0 plus a 0-1 edge: no cycle of length >= 3.
212        let mut g = Graph::with_vertices(2);
213        g.add_edge(0, 0).unwrap();
214        g.add_edge(0, 1).unwrap();
215        assert_eq!(girth(&g).unwrap(), None);
216    }
217
218    #[test]
219    fn parallel_edges_do_not_count_as_cycle() {
220        // Two parallel edges between 0 and 1: no cycle of length >= 3
221        // (multi-edges form a 2-cycle which we ignore per upstream).
222        let mut g = Graph::with_vertices(2);
223        g.add_edge(0, 1).unwrap();
224        g.add_edge(0, 1).unwrap();
225        assert_eq!(girth(&g).unwrap(), None);
226    }
227
228    #[test]
229    fn two_components_picks_smaller_girth() {
230        // Component A: 0-1-2-3-0 (4-cycle, girth 4).
231        // Component B: 4-5-6-4 (3-cycle, girth 3).
232        // Overall girth: 3 (min of the two).
233        let mut g = Graph::with_vertices(7);
234        for i in 0..4u32 {
235            g.add_edge(i, (i + 1) % 4).unwrap();
236        }
237        g.add_edge(4, 5).unwrap();
238        g.add_edge(5, 6).unwrap();
239        g.add_edge(6, 4).unwrap();
240        assert_eq!(girth(&g).unwrap(), Some(3));
241    }
242
243    #[test]
244    fn pendant_does_not_change_cycle_girth() {
245        // Triangle + pendant edge to a fourth vertex.
246        let mut g = Graph::with_vertices(4);
247        g.add_edge(0, 1).unwrap();
248        g.add_edge(1, 2).unwrap();
249        g.add_edge(2, 0).unwrap();
250        g.add_edge(2, 3).unwrap();
251        assert_eq!(girth(&g).unwrap(), Some(3));
252    }
253}