Skip to main content

rust_igraph/algorithms/paths/
eulerian.rs

1//! Eulerian path / cycle existence (ALGO-CC-040).
2//!
3//! Counterpart of `igraph_is_eulerian()` from
4//! `references/igraph/src/paths/eulerian.c:333` (and its undirected /
5//! directed helpers above). Determines whether the graph admits an
6//! Eulerian path (visits every edge exactly once) and/or cycle (closed
7//! Eulerian path).
8//!
9//! Algorithm (per upstream):
10//! - Trivial: empty edge set or `n <= 1` → both true.
11//! - Weak-connectivity check on the non-singleton part: at most one
12//!   weak component may contain edges.
13//! - Singleton accounting: at most one singleton-with-self-loops
14//!   vertex may exist, and it cannot coexist with non-singleton edges.
15//! - Undirected: count odd-degree vertices (self-loops count twice).
16//!   `odd > 2` → neither; `odd == 2` → path only; `odd == 0` → both.
17//! - Directed: each vertex must have `in_degree == out_degree` for a
18//!   cycle; for a path one vertex may have `out - in == 1` and one
19//!   `in - out == 1`. Larger imbalances → neither.
20//!
21//! Phase-1 minimal slice ships only the *existence* test. Concrete
22//! path/cycle construction (Hierholzer) ships in CC-041 / CC-042.
23
24use crate::core::{Graph, IgraphResult, VertexId};
25
26/// Whether an Eulerian path or cycle exists in a graph.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct EulerianClassification {
29    /// `true` iff there exists a walk that traverses every edge exactly once.
30    pub has_path: bool,
31    /// `true` iff there exists a *closed* walk that traverses every edge
32    /// exactly once. Implies `has_path`.
33    pub has_cycle: bool,
34}
35
36/// Decide whether `graph` has an Eulerian path and/or cycle.
37///
38/// Counterpart of `igraph_is_eulerian()` from
39/// `references/igraph/src/paths/eulerian.c:333`.
40///
41/// # Examples
42///
43/// ```
44/// use rust_igraph::{Graph, is_eulerian};
45///
46/// // Triangle (3-cycle): every vertex has even degree → both path & cycle.
47/// let mut g = Graph::with_vertices(3);
48/// g.add_edge(0, 1).unwrap();
49/// g.add_edge(1, 2).unwrap();
50/// g.add_edge(2, 0).unwrap();
51/// let r = is_eulerian(&g).unwrap();
52/// assert!(r.has_path && r.has_cycle);
53///
54/// // Path 0-1-2: vertices 0, 2 have odd degree → path only, no cycle.
55/// let mut g = Graph::with_vertices(3);
56/// g.add_edge(0, 1).unwrap();
57/// g.add_edge(1, 2).unwrap();
58/// let r = is_eulerian(&g).unwrap();
59/// assert!(r.has_path && !r.has_cycle);
60/// ```
61pub fn is_eulerian(graph: &Graph) -> IgraphResult<EulerianClassification> {
62    let n = graph.vcount();
63    let m = graph.ecount();
64    if m == 0 || n <= 1 {
65        return Ok(EulerianClassification {
66            has_path: true,
67            has_cycle: true,
68        });
69    }
70    if graph.is_directed() {
71        is_eulerian_directed(graph)
72    } else {
73        is_eulerian_undirected(graph)
74    }
75}
76
77const NONE: EulerianClassification = EulerianClassification {
78    has_path: false,
79    has_cycle: false,
80};
81
82/// Connectivity precondition shared by the directed and undirected
83/// variants: the graph may have at most one weak component of size > 1.
84/// (Singletons — vertices with no incident edges or only self-loops —
85/// don't affect Eulerianness on the connectivity axis.)
86fn at_most_one_nonsingleton_component(graph: &Graph) -> IgraphResult<bool> {
87    let cc = crate::algorithms::connectivity::connected_components(graph)?;
88    let n = graph.vcount() as usize;
89    let mut sizes = vec![0u32; cc.count as usize];
90    for &m in &cc.membership {
91        sizes[m as usize] = sizes[m as usize].saturating_add(1);
92        let _ = n; // silence unused-binding warnings on smaller graphs
93    }
94    let big = sizes.into_iter().filter(|&s| s > 1).count();
95    Ok(big <= 1)
96}
97
98/// Number of self-loops incident to `v` (counts each loop edge once).
99fn self_loop_count(graph: &Graph, v: VertexId) -> IgraphResult<usize> {
100    // For undirected graphs `neighbors(v)` reports each self-loop twice
101    // (LOOPS_TWICE convention). Halve the count to recover edge multiplicity.
102    let raw = graph.neighbors(v)?.iter().filter(|&&u| u == v).count();
103    if graph.is_directed() {
104        Ok(raw)
105    } else {
106        Ok(raw / 2)
107    }
108}
109
110fn is_eulerian_undirected(graph: &Graph) -> IgraphResult<EulerianClassification> {
111    if !at_most_one_nonsingleton_component(graph)? {
112        return Ok(NONE);
113    }
114
115    let n = graph.vcount();
116
117    let mut odd: u32 = 0;
118    let mut singleton_self_loop: u32 = 0; // upstream `es`
119    let mut has_nonsingleton_edge: u32 = 0; // upstream `ens`
120
121    for v in 0..n {
122        let total_deg = graph.degree(v)?; // LOOPS_TWICE
123        if total_deg == 0 {
124            continue;
125        }
126        let loops = self_loop_count(graph, v)?;
127        // upstream IGRAPH_NO_LOOPS degree:
128        //   undirected NO_LOOPS = LOOPS_TWICE - 2 * loop_count
129        let nonloop_deg = total_deg - 2 * loops;
130        if nonloop_deg == 0 {
131            // singleton with self-loop(s)
132            singleton_self_loop = singleton_self_loop.saturating_add(1);
133        } else {
134            has_nonsingleton_edge = 1;
135            // Self-loops counted twice (LOOPS_TWICE), so total_deg's
136            // parity already mirrors upstream's odd-degree check.
137            if total_deg % 2 != 0 {
138                odd = odd.saturating_add(1);
139            }
140        }
141        if singleton_self_loop + has_nonsingleton_edge > 1 {
142            return Ok(NONE);
143        }
144    }
145
146    let (has_path, has_cycle) = match odd.cmp(&2) {
147        std::cmp::Ordering::Greater => (false, false),
148        std::cmp::Ordering::Equal => (true, false),
149        std::cmp::Ordering::Less => (true, true),
150    };
151    Ok(EulerianClassification {
152        has_path,
153        has_cycle,
154    })
155}
156
157fn is_eulerian_directed(graph: &Graph) -> IgraphResult<EulerianClassification> {
158    if !at_most_one_nonsingleton_component(graph)? {
159        return Ok(NONE);
160    }
161    let n = graph.vcount();
162
163    let mut incoming_excess: u32 = 0;
164    let mut outgoing_excess: u32 = 0;
165    let mut singleton_self_loop: u32 = 0;
166    let mut has_nonsingleton_edge: u32 = 0;
167
168    for v in 0..n {
169        let out_deg = graph.out_neighbors_vec(v)?.len();
170        let in_deg = graph.in_neighbors_vec(v)?.len();
171        if out_deg + in_deg == 0 {
172            continue;
173        }
174        let loops = self_loop_count(graph, v)?;
175        // upstream's `nonsingleton` is degree(IGRAPH_ALL, NO_LOOPS).
176        // For directed, NO_LOOPS_ALL = (out_deg + in_deg) - 2 * loops.
177        let nonloop_deg = out_deg + in_deg - 2 * loops;
178        if nonloop_deg == 0 {
179            singleton_self_loop = singleton_self_loop.saturating_add(1);
180        } else {
181            has_nonsingleton_edge = 1;
182        }
183        if singleton_self_loop + has_nonsingleton_edge > 1 {
184            return Ok(NONE);
185        }
186
187        // Per-vertex in/out balance (self-loops contribute equally to both
188        // — they cancel automatically).
189        if in_deg == out_deg {
190            continue;
191        }
192        if in_deg > out_deg {
193            let diff = u32::try_from(in_deg - out_deg)
194                .map_err(|_| crate::core::IgraphError::Internal("degree overflow"))?;
195            incoming_excess = incoming_excess.saturating_add(diff);
196        } else {
197            let diff = u32::try_from(out_deg - in_deg)
198                .map_err(|_| crate::core::IgraphError::Internal("degree overflow"))?;
199            outgoing_excess = outgoing_excess.saturating_add(diff);
200        }
201        if incoming_excess > 1 || outgoing_excess > 1 {
202            return Ok(NONE);
203        }
204    }
205
206    let has_cycle = incoming_excess == 0 && outgoing_excess == 0;
207    // upstream sets has_path = true at this point unconditionally:
208    // any imbalance survived above must be the (1, 1) "path only" case.
209    Ok(EulerianClassification {
210        has_path: true,
211        has_cycle,
212    })
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    fn classify(graph: &Graph) -> EulerianClassification {
220        is_eulerian(graph).unwrap()
221    }
222
223    #[test]
224    fn empty_graph_is_eulerian() {
225        let g = Graph::with_vertices(0);
226        assert_eq!(
227            classify(&g),
228            EulerianClassification {
229                has_path: true,
230                has_cycle: true
231            }
232        );
233    }
234
235    #[test]
236    fn single_isolated_vertex_is_eulerian() {
237        let g = Graph::with_vertices(1);
238        assert_eq!(
239            classify(&g),
240            EulerianClassification {
241                has_path: true,
242                has_cycle: true
243            }
244        );
245    }
246
247    #[test]
248    fn isolated_vertices_no_edges_are_eulerian() {
249        let g = Graph::with_vertices(5);
250        assert_eq!(
251            classify(&g),
252            EulerianClassification {
253                has_path: true,
254                has_cycle: true
255            }
256        );
257    }
258
259    #[test]
260    fn undirected_path_has_path_no_cycle() {
261        let mut g = Graph::with_vertices(3);
262        g.add_edge(0, 1).unwrap();
263        g.add_edge(1, 2).unwrap();
264        let r = classify(&g);
265        assert!(r.has_path);
266        assert!(!r.has_cycle);
267    }
268
269    #[test]
270    fn undirected_triangle_has_cycle_and_path() {
271        let mut g = Graph::with_vertices(3);
272        g.add_edge(0, 1).unwrap();
273        g.add_edge(1, 2).unwrap();
274        g.add_edge(2, 0).unwrap();
275        let r = classify(&g);
276        assert!(r.has_path);
277        assert!(r.has_cycle);
278    }
279
280    #[test]
281    fn undirected_disconnected_components_are_not_eulerian() {
282        let mut g = Graph::with_vertices(4);
283        g.add_edge(0, 1).unwrap();
284        g.add_edge(2, 3).unwrap();
285        let r = classify(&g);
286        assert!(!r.has_path);
287        assert!(!r.has_cycle);
288    }
289
290    #[test]
291    fn undirected_too_many_odd_vertices_is_not_eulerian() {
292        // K4: every vertex has degree 3 → 4 odd vertices → not Eulerian.
293        let mut g = Graph::with_vertices(4);
294        for u in 0..4u32 {
295            for v in (u + 1)..4 {
296                g.add_edge(u, v).unwrap();
297            }
298        }
299        let r = classify(&g);
300        assert!(!r.has_path);
301        assert!(!r.has_cycle);
302    }
303
304    #[test]
305    fn undirected_self_loop_on_otherwise_eulerian_graph() {
306        // Self-loop adds 2 to degree → keeps parities, so triangle + self-loop
307        // is still Eulerian (path AND cycle).
308        let mut g = Graph::with_vertices(3);
309        g.add_edge(0, 0).unwrap();
310        g.add_edge(0, 1).unwrap();
311        g.add_edge(1, 2).unwrap();
312        g.add_edge(2, 0).unwrap();
313        let r = classify(&g);
314        assert!(r.has_path);
315        assert!(r.has_cycle);
316    }
317
318    #[test]
319    fn directed_cycle_has_cycle() {
320        // 0 -> 1 -> 2 -> 0
321        let mut g = Graph::new(3, true).unwrap();
322        g.add_edge(0, 1).unwrap();
323        g.add_edge(1, 2).unwrap();
324        g.add_edge(2, 0).unwrap();
325        let r = classify(&g);
326        assert!(r.has_path);
327        assert!(r.has_cycle);
328    }
329
330    #[test]
331    fn directed_path_has_path_no_cycle() {
332        // 0 -> 1 -> 2: out_excess(0)=1, in_excess(2)=1 → path only.
333        let mut g = Graph::new(3, true).unwrap();
334        g.add_edge(0, 1).unwrap();
335        g.add_edge(1, 2).unwrap();
336        let r = classify(&g);
337        assert!(r.has_path);
338        assert!(!r.has_cycle);
339    }
340
341    #[test]
342    fn directed_too_imbalanced_is_not_eulerian() {
343        // Vertex 0 has out_degree 2 in_degree 0; not Eulerian.
344        let mut g = Graph::new(3, true).unwrap();
345        g.add_edge(0, 1).unwrap();
346        g.add_edge(0, 2).unwrap();
347        let r = classify(&g);
348        assert!(!r.has_path);
349        assert!(!r.has_cycle);
350    }
351}