Skip to main content

rust_igraph/algorithms/paths/
eulerian_construct.rs

1//! Eulerian path / cycle construction (ALGO-CC-041).
2//!
3//! Counterpart of `igraph_eulerian_path()` and `igraph_eulerian_cycle()`
4//! from `references/igraph/src/paths/eulerian.c:345-450` (the undirected
5//! Hierholzer driver). Returns the sequence of edge ids that traverse
6//! every edge exactly once.
7//!
8//! Phase-1 minimal slice: undirected only. Directed Hierholzer (different
9//! adjacency tracking — see `igraph_i_eulerian_path_directed` at
10//! `eulerian.c:453+`) ships in CC-042.
11
12use crate::algorithms::paths::eulerian::is_eulerian;
13use crate::core::graph::EdgeId;
14use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
15
16/// Build an Eulerian cycle if one exists, or return an error.
17///
18/// Unlike [`eulerian_path`], this function requires that every vertex
19/// with nonzero degree has even degree (undirected) or equal in-/out-degree
20/// (directed). Returns `Err` if no Eulerian cycle exists.
21///
22/// Counterpart of `igraph_eulerian_cycle()` from
23/// `references/igraph/src/paths/eulerian.c:594`.
24///
25/// # Examples
26///
27/// ```
28/// use rust_igraph::{Graph, eulerian_cycle};
29///
30/// // Triangle 0-1-2-0: every vertex has even degree → cycle exists.
31/// let mut g = Graph::with_vertices(3);
32/// g.add_edge(0, 1).unwrap();
33/// g.add_edge(1, 2).unwrap();
34/// g.add_edge(2, 0).unwrap();
35/// let cycle = eulerian_cycle(&g).unwrap();
36/// assert_eq!(cycle.len(), 3);
37///
38/// // Path 0-1-2: has Euler path but no Euler cycle → error.
39/// let mut g = Graph::with_vertices(3);
40/// g.add_edge(0, 1).unwrap();
41/// g.add_edge(1, 2).unwrap();
42/// assert!(eulerian_cycle(&g).is_err());
43/// ```
44pub fn eulerian_cycle(graph: &Graph) -> IgraphResult<Vec<EdgeId>> {
45    let cls = is_eulerian(graph)?;
46    if !cls.has_cycle {
47        return Err(IgraphError::InvalidArgument(
48            "the graph does not have an Eulerian cycle".to_string(),
49        ));
50    }
51
52    let m = graph.ecount();
53    if m == 0 {
54        return Ok(Vec::new());
55    }
56
57    // Delegate to the Hierholzer implementation in eulerian_path.
58    // Since has_cycle is true, eulerian_path will also succeed and
59    // produce a cycle (the walk starts and ends at the same vertex).
60    match eulerian_path(graph)? {
61        Some(edges) => Ok(edges),
62        None => Err(IgraphError::Internal(
63            "has_cycle is true but eulerian_path returned None",
64        )),
65    }
66}
67
68/// Build an Eulerian path or cycle for `graph` if one exists.
69/// Returns `Some(edge_ids)` (the walk visits each edge exactly once)
70/// or `None` if no Eulerian walk exists.
71///
72/// Counterpart of `igraph_eulerian_path()` (returns the path if any)
73/// from `references/igraph/src/paths/eulerian.c:345`. Undirected only
74/// in this slice; directed graphs return an error.
75///
76/// # Examples
77///
78/// ```
79/// use rust_igraph::{Graph, eulerian_path};
80///
81/// // Triangle 0-1-2-0: every vertex has even degree → Euler cycle exists.
82/// let mut g = Graph::with_vertices(3);
83/// g.add_edge(0, 1).unwrap();   // edge 0
84/// g.add_edge(1, 2).unwrap();   // edge 1
85/// g.add_edge(2, 0).unwrap();   // edge 2
86/// let walk = eulerian_path(&g).unwrap().unwrap();
87/// assert_eq!(walk.len(), 3);
88///
89/// // Path 0-1-2: 2 odd-degree vertices → Euler path (no cycle).
90/// let mut g = Graph::with_vertices(3);
91/// g.add_edge(0, 1).unwrap();
92/// g.add_edge(1, 2).unwrap();
93/// let walk = eulerian_path(&g).unwrap().unwrap();
94/// assert_eq!(walk.len(), 2);
95///
96/// // K4: 4 odd-degree vertices → no Euler path.
97/// let mut g = Graph::with_vertices(4);
98/// for u in 0..4u32 {
99///     for v in (u + 1)..4 {
100///         g.add_edge(u, v).unwrap();
101///     }
102/// }
103/// assert_eq!(eulerian_path(&g).unwrap(), None);
104/// ```
105pub fn eulerian_path(graph: &Graph) -> IgraphResult<Option<Vec<EdgeId>>> {
106    let cls = is_eulerian(graph)?;
107    if !cls.has_path {
108        return Ok(None);
109    }
110
111    let n = graph.vcount();
112    let m = graph.ecount();
113    if m == 0 {
114        // Empty walk for graphs with no edges (still trivially Eulerian).
115        return Ok(Some(Vec::new()));
116    }
117
118    let n_us = n as usize;
119    let m_us = m;
120    let directed = graph.is_directed();
121
122    // Per-vertex incident-edge lists. For undirected graphs `incident()`
123    // returns LOOPS_TWICE (self-loops appear twice); upstream's Hierholzer
124    // uses LOOPS_ONCE so we dedupe. For directed graphs we use the `oi`-side
125    // out-edges directly via `incident()`'s directed branch (same semantics
126    // as upstream's `IGRAPH_OUT` mode).
127    let mut inc: Vec<Vec<EdgeId>> = Vec::with_capacity(n_us);
128    for v in 0..n {
129        let raw = graph.incident(v)?;
130        if directed {
131            // Already out-only and one entry per edge.
132            inc.push(raw);
133        } else {
134            let mut seen: std::collections::HashSet<EdgeId> = std::collections::HashSet::new();
135            let mut out: Vec<EdgeId> = Vec::with_capacity(raw.len());
136            for e in raw {
137                if seen.insert(e) {
138                    out.push(e);
139                }
140            }
141            inc.push(out);
142        }
143    }
144
145    // Track simple "remaining degree" per vertex via the count of unvisited
146    // incident edges. Upstream uses `igraph_degree(_, IGRAPH_LOOPS)` which
147    // counts self-loops twice; we use the pre-built inc list and the visited
148    // bitset.
149    let mut visited: Vec<bool> = vec![false; m_us];
150    let mut next_idx: Vec<usize> = vec![0; n_us];
151
152    // Pick the start vertex: per upstream's logic in is_eulerian helpers,
153    // it's an odd-degree vertex if `has_path && !has_cycle`, otherwise
154    // any vertex with a non-zero unvisited incident edge.
155    let start_of_path = pick_start_vertex(graph, cls)?;
156
157    // Hierholzer's algorithm (iterative). Two stacks: `tracker` is the
158    // current walk; `path` is the output (built in reverse).
159    let mut tracker: Vec<VertexId> = Vec::with_capacity(n_us);
160    let mut edge_tracker: Vec<EdgeId> = Vec::with_capacity(m_us);
161    let mut path: Vec<VertexId> = Vec::with_capacity(n_us);
162    let mut edge_path: Vec<EdgeId> = Vec::with_capacity(m_us);
163
164    tracker.push(start_of_path);
165    let mut curr = start_of_path;
166
167    loop {
168        // Advance through `curr`'s next unvisited incident edge, if any.
169        let curr_us = curr as usize;
170        // Skip already-visited edges in the per-vertex iterator.
171        while next_idx[curr_us] < inc[curr_us].len()
172            && visited[inc[curr_us][next_idx[curr_us]] as usize]
173        {
174            next_idx[curr_us] += 1;
175        }
176        if next_idx[curr_us] < inc[curr_us].len() {
177            let edge = inc[curr_us][next_idx[curr_us]];
178            visited[edge as usize] = true;
179            next_idx[curr_us] += 1;
180            tracker.push(curr);
181            edge_tracker.push(edge);
182            curr = if directed {
183                // Directed: edge always points from curr → to[edge].
184                graph.edge_target(edge)?
185            } else {
186                graph.edge_other(edge, curr)?
187            };
188        } else {
189            // Dead end at `curr`: pop the walk.
190            path.push(curr);
191            if let Some(prev) = tracker.pop() {
192                if let Some(curr_e) = edge_tracker.pop() {
193                    edge_path.push(curr_e);
194                }
195                curr = prev;
196            } else {
197                break;
198            }
199        }
200    }
201
202    // edge_path was filled with the walk in reverse; reverse it now to get
203    // the forward edge order. (Upstream pops to the result vector, which
204    // also reverses the order.)
205    edge_path.reverse();
206    let _ = path; // vertex sequence — not part of the Phase-1 return
207
208    Ok(Some(edge_path))
209}
210
211fn pick_start_vertex(
212    graph: &Graph,
213    cls: crate::algorithms::paths::eulerian::EulerianClassification,
214) -> IgraphResult<VertexId> {
215    let n = graph.vcount();
216    let directed = graph.is_directed();
217    if cls.has_cycle {
218        // Any vertex with non-zero (out-)degree.
219        for v in 0..n {
220            let has_edges = if directed {
221                !graph.incident(v)?.is_empty()
222            } else {
223                !graph.neighbors(v)?.is_empty()
224            };
225            if has_edges {
226                return Ok(v);
227            }
228        }
229        // Should be unreachable since `m == 0` returns early above.
230        Err(IgraphError::Internal("no edges but cls.has_cycle"))
231    } else if directed {
232        // Directed path: pick the vertex with `out_degree - in_degree == 1`
233        // (the unique source per is_eulerian's outgoing_excess accounting).
234        for v in 0..n {
235            let out_deg = graph.incident(v)?.len();
236            // in-degree via the in-side
237            let in_deg = compute_in_degree(graph, v)?;
238            if out_deg > in_deg && (out_deg - in_deg) == 1 {
239                return Ok(v);
240            }
241        }
242        Err(IgraphError::Internal(
243            "directed path: no vertex with outgoing_excess == 1",
244        ))
245    } else {
246        // Undirected path: smallest-id odd-degree vertex.
247        for v in 0..n {
248            let deg = graph.degree(v)?;
249            if deg % 2 != 0 {
250                return Ok(v);
251            }
252        }
253        Err(IgraphError::Internal(
254            "has_path && !has_cycle but no odd-degree vertex found",
255        ))
256    }
257}
258
259fn compute_in_degree(graph: &Graph, v: VertexId) -> IgraphResult<usize> {
260    // Total degree via undirected `degree` would mix in/out; use a fresh
261    // count via in-side inferred from edges (n times m worst case is fine
262    // here — only called once per vertex during start selection).
263    let mut count = 0usize;
264    let m = u32::try_from(graph.ecount()).expect("edge count fits in u32");
265    for e in 0..m {
266        if graph.edge_target(e)? == v {
267            count += 1;
268        }
269    }
270    Ok(count)
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    fn walk_validates(graph: &Graph, walk: &[EdgeId]) -> bool {
278        // Each edge appears exactly once.
279        let mut seen: Vec<bool> = vec![false; graph.ecount()];
280        for &edge in walk {
281            let idx = edge as usize;
282            if idx >= graph.ecount() || seen[idx] {
283                return false;
284            }
285            seen[idx] = true;
286        }
287        // Walk is consecutively connected.
288        if walk.len() < 2 {
289            return true;
290        }
291        for i in 0..walk.len() - 1 {
292            let (a, b) = graph.edge(walk[i]).unwrap();
293            let (c, d) = graph.edge(walk[i + 1]).unwrap();
294            if !(a == c || a == d || b == c || b == d) {
295                return false;
296            }
297        }
298        true
299    }
300
301    #[test]
302    fn empty_graph_returns_empty_walk() {
303        let g = Graph::with_vertices(0);
304        assert_eq!(eulerian_path(&g).unwrap(), Some(Vec::new()));
305    }
306
307    #[test]
308    fn isolated_vertices_return_empty_walk() {
309        let g = Graph::with_vertices(5);
310        assert_eq!(eulerian_path(&g).unwrap(), Some(Vec::new()));
311    }
312
313    #[test]
314    fn triangle_yields_three_edge_walk() {
315        let mut g = Graph::with_vertices(3);
316        g.add_edge(0, 1).unwrap();
317        g.add_edge(1, 2).unwrap();
318        g.add_edge(2, 0).unwrap();
319        let walk = eulerian_path(&g).unwrap().unwrap();
320        assert_eq!(walk.len(), 3);
321        assert!(walk_validates(&g, &walk));
322    }
323
324    #[test]
325    fn path_3_yields_two_edge_walk() {
326        let mut g = Graph::with_vertices(3);
327        g.add_edge(0, 1).unwrap();
328        g.add_edge(1, 2).unwrap();
329        let walk = eulerian_path(&g).unwrap().unwrap();
330        assert_eq!(walk.len(), 2);
331        assert!(walk_validates(&g, &walk));
332    }
333
334    #[test]
335    fn k4_has_no_eulerian_walk() {
336        let mut g = Graph::with_vertices(4);
337        for u in 0..4u32 {
338            for v in (u + 1)..4 {
339                g.add_edge(u, v).unwrap();
340            }
341        }
342        assert_eq!(eulerian_path(&g).unwrap(), None);
343    }
344
345    #[test]
346    fn disconnected_components_no_walk() {
347        let mut g = Graph::with_vertices(4);
348        g.add_edge(0, 1).unwrap();
349        g.add_edge(2, 3).unwrap();
350        assert_eq!(eulerian_path(&g).unwrap(), None);
351    }
352
353    #[test]
354    fn ring_5_walk_visits_all_edges() {
355        let mut g = Graph::with_vertices(5);
356        for i in 0..5u32 {
357            g.add_edge(i, (i + 1) % 5).unwrap();
358        }
359        let walk = eulerian_path(&g).unwrap().unwrap();
360        assert_eq!(walk.len(), 5);
361        assert!(walk_validates(&g, &walk));
362    }
363
364    #[test]
365    fn directed_3_cycle_yields_3_edge_walk() {
366        // 0 -> 1 -> 2 -> 0: directed cycle, has Eulerian cycle.
367        let mut g = Graph::new(3, true).unwrap();
368        g.add_edge(0, 1).unwrap();
369        g.add_edge(1, 2).unwrap();
370        g.add_edge(2, 0).unwrap();
371        let walk = eulerian_path(&g).unwrap().unwrap();
372        assert_eq!(walk.len(), 3);
373    }
374
375    #[test]
376    fn directed_path_yields_chain_walk() {
377        // 0 -> 1 -> 2: directed Eulerian path (out_excess at 0 = 1).
378        let mut g = Graph::new(3, true).unwrap();
379        g.add_edge(0, 1).unwrap();
380        g.add_edge(1, 2).unwrap();
381        let walk = eulerian_path(&g).unwrap().unwrap();
382        assert_eq!(walk, vec![0, 1]); // edges traversed in order
383    }
384
385    #[test]
386    fn directed_no_eulerian_returns_none() {
387        // 0 -> 1 -> 2 plus 0 -> 2: two out-edges from 0, two in to 2 → no path.
388        let mut g = Graph::new(3, true).unwrap();
389        g.add_edge(0, 1).unwrap();
390        g.add_edge(1, 2).unwrap();
391        g.add_edge(0, 2).unwrap();
392        assert_eq!(eulerian_path(&g).unwrap(), None);
393    }
394
395    // ---- eulerian_cycle tests ----
396
397    #[test]
398    fn cycle_triangle() {
399        let mut g = Graph::with_vertices(3);
400        g.add_edge(0, 1).unwrap();
401        g.add_edge(1, 2).unwrap();
402        g.add_edge(2, 0).unwrap();
403        let cycle = eulerian_cycle(&g).unwrap();
404        assert_eq!(cycle.len(), 3);
405        assert!(walk_validates(&g, &cycle));
406    }
407
408    #[test]
409    fn cycle_ring5() {
410        let mut g = Graph::with_vertices(5);
411        for i in 0..5u32 {
412            g.add_edge(i, (i + 1) % 5).unwrap();
413        }
414        let cycle = eulerian_cycle(&g).unwrap();
415        assert_eq!(cycle.len(), 5);
416        assert!(walk_validates(&g, &cycle));
417    }
418
419    #[test]
420    fn cycle_empty_graph() {
421        let g = Graph::with_vertices(0);
422        let cycle = eulerian_cycle(&g).unwrap();
423        assert!(cycle.is_empty());
424    }
425
426    #[test]
427    fn cycle_isolated_vertices() {
428        let g = Graph::with_vertices(4);
429        let cycle = eulerian_cycle(&g).unwrap();
430        assert!(cycle.is_empty());
431    }
432
433    #[test]
434    fn cycle_path_graph_errors() {
435        // Path 0-1-2 has Euler path but no cycle.
436        let mut g = Graph::with_vertices(3);
437        g.add_edge(0, 1).unwrap();
438        g.add_edge(1, 2).unwrap();
439        assert!(eulerian_cycle(&g).is_err());
440    }
441
442    #[test]
443    fn cycle_k4_errors() {
444        // K4: 4 odd-degree vertices → no Euler path or cycle.
445        let mut g = Graph::with_vertices(4);
446        for u in 0..4u32 {
447            for v in (u + 1)..4 {
448                g.add_edge(u, v).unwrap();
449            }
450        }
451        assert!(eulerian_cycle(&g).is_err());
452    }
453
454    #[test]
455    fn cycle_directed_3cycle() {
456        let mut g = Graph::new(3, true).unwrap();
457        g.add_edge(0, 1).unwrap();
458        g.add_edge(1, 2).unwrap();
459        g.add_edge(2, 0).unwrap();
460        let cycle = eulerian_cycle(&g).unwrap();
461        assert_eq!(cycle.len(), 3);
462    }
463
464    #[test]
465    fn cycle_directed_path_errors() {
466        // 0->1->2: directed Euler path but no cycle.
467        let mut g = Graph::new(3, true).unwrap();
468        g.add_edge(0, 1).unwrap();
469        g.add_edge(1, 2).unwrap();
470        assert!(eulerian_cycle(&g).is_err());
471    }
472
473    #[test]
474    fn complex_eulerian_path_test_eulerian_r() {
475        // R test-eulerian.R 6-vertex literal-graph case has Euler path but no cycle.
476        // Edges: 0-1, 1-2, 2-3, 3-4, 4-0, 0-5, 5-3, 3-1, 1-5, 5-4 (10 edges).
477        let mut g = Graph::with_vertices(6);
478        for &(u, v) in &[
479            (0, 1),
480            (1, 2),
481            (2, 3),
482            (3, 4),
483            (4, 0),
484            (0, 5),
485            (5, 3),
486            (3, 1),
487            (1, 5),
488            (5, 4),
489        ] {
490            g.add_edge(u, v).unwrap();
491        }
492        let walk = eulerian_path(&g).unwrap().unwrap();
493        assert_eq!(walk.len(), 10, "must visit every edge exactly once");
494        assert!(walk_validates(&g, &walk));
495    }
496}