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_iter(v)?.len() > 0
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())
265        .map_err(|_| IgraphError::InvalidArgument("edge count exceeds u32 range".into()))?;
266    for e in 0..m {
267        if graph.edge_target(e)? == v {
268            count += 1;
269        }
270    }
271    Ok(count)
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    fn walk_validates(graph: &Graph, walk: &[EdgeId]) -> bool {
279        // Each edge appears exactly once.
280        let mut seen: Vec<bool> = vec![false; graph.ecount()];
281        for &edge in walk {
282            let idx = edge as usize;
283            if idx >= graph.ecount() || seen[idx] {
284                return false;
285            }
286            seen[idx] = true;
287        }
288        // Walk is consecutively connected.
289        if walk.len() < 2 {
290            return true;
291        }
292        for i in 0..walk.len() - 1 {
293            let (a, b) = graph.edge(walk[i]).unwrap();
294            let (c, d) = graph.edge(walk[i + 1]).unwrap();
295            if !(a == c || a == d || b == c || b == d) {
296                return false;
297            }
298        }
299        true
300    }
301
302    #[test]
303    fn empty_graph_returns_empty_walk() {
304        let g = Graph::with_vertices(0);
305        assert_eq!(eulerian_path(&g).unwrap(), Some(Vec::new()));
306    }
307
308    #[test]
309    fn isolated_vertices_return_empty_walk() {
310        let g = Graph::with_vertices(5);
311        assert_eq!(eulerian_path(&g).unwrap(), Some(Vec::new()));
312    }
313
314    #[test]
315    fn triangle_yields_three_edge_walk() {
316        let mut g = Graph::with_vertices(3);
317        g.add_edge(0, 1).unwrap();
318        g.add_edge(1, 2).unwrap();
319        g.add_edge(2, 0).unwrap();
320        let walk = eulerian_path(&g).unwrap().unwrap();
321        assert_eq!(walk.len(), 3);
322        assert!(walk_validates(&g, &walk));
323    }
324
325    #[test]
326    fn path_3_yields_two_edge_walk() {
327        let mut g = Graph::with_vertices(3);
328        g.add_edge(0, 1).unwrap();
329        g.add_edge(1, 2).unwrap();
330        let walk = eulerian_path(&g).unwrap().unwrap();
331        assert_eq!(walk.len(), 2);
332        assert!(walk_validates(&g, &walk));
333    }
334
335    #[test]
336    fn k4_has_no_eulerian_walk() {
337        let mut g = Graph::with_vertices(4);
338        for u in 0..4u32 {
339            for v in (u + 1)..4 {
340                g.add_edge(u, v).unwrap();
341            }
342        }
343        assert_eq!(eulerian_path(&g).unwrap(), None);
344    }
345
346    #[test]
347    fn disconnected_components_no_walk() {
348        let mut g = Graph::with_vertices(4);
349        g.add_edge(0, 1).unwrap();
350        g.add_edge(2, 3).unwrap();
351        assert_eq!(eulerian_path(&g).unwrap(), None);
352    }
353
354    #[test]
355    fn ring_5_walk_visits_all_edges() {
356        let mut g = Graph::with_vertices(5);
357        for i in 0..5u32 {
358            g.add_edge(i, (i + 1) % 5).unwrap();
359        }
360        let walk = eulerian_path(&g).unwrap().unwrap();
361        assert_eq!(walk.len(), 5);
362        assert!(walk_validates(&g, &walk));
363    }
364
365    #[test]
366    fn directed_3_cycle_yields_3_edge_walk() {
367        // 0 -> 1 -> 2 -> 0: directed cycle, has Eulerian cycle.
368        let mut g = Graph::new(3, true).unwrap();
369        g.add_edge(0, 1).unwrap();
370        g.add_edge(1, 2).unwrap();
371        g.add_edge(2, 0).unwrap();
372        let walk = eulerian_path(&g).unwrap().unwrap();
373        assert_eq!(walk.len(), 3);
374    }
375
376    #[test]
377    fn directed_path_yields_chain_walk() {
378        // 0 -> 1 -> 2: directed Eulerian path (out_excess at 0 = 1).
379        let mut g = Graph::new(3, true).unwrap();
380        g.add_edge(0, 1).unwrap();
381        g.add_edge(1, 2).unwrap();
382        let walk = eulerian_path(&g).unwrap().unwrap();
383        assert_eq!(walk, vec![0, 1]); // edges traversed in order
384    }
385
386    #[test]
387    fn directed_no_eulerian_returns_none() {
388        // 0 -> 1 -> 2 plus 0 -> 2: two out-edges from 0, two in to 2 → no path.
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(0, 2).unwrap();
393        assert_eq!(eulerian_path(&g).unwrap(), None);
394    }
395
396    // ---- eulerian_cycle tests ----
397
398    #[test]
399    fn cycle_triangle() {
400        let mut g = Graph::with_vertices(3);
401        g.add_edge(0, 1).unwrap();
402        g.add_edge(1, 2).unwrap();
403        g.add_edge(2, 0).unwrap();
404        let cycle = eulerian_cycle(&g).unwrap();
405        assert_eq!(cycle.len(), 3);
406        assert!(walk_validates(&g, &cycle));
407    }
408
409    #[test]
410    fn cycle_ring5() {
411        let mut g = Graph::with_vertices(5);
412        for i in 0..5u32 {
413            g.add_edge(i, (i + 1) % 5).unwrap();
414        }
415        let cycle = eulerian_cycle(&g).unwrap();
416        assert_eq!(cycle.len(), 5);
417        assert!(walk_validates(&g, &cycle));
418    }
419
420    #[test]
421    fn cycle_empty_graph() {
422        let g = Graph::with_vertices(0);
423        let cycle = eulerian_cycle(&g).unwrap();
424        assert!(cycle.is_empty());
425    }
426
427    #[test]
428    fn cycle_isolated_vertices() {
429        let g = Graph::with_vertices(4);
430        let cycle = eulerian_cycle(&g).unwrap();
431        assert!(cycle.is_empty());
432    }
433
434    #[test]
435    fn cycle_path_graph_errors() {
436        // Path 0-1-2 has Euler path but no cycle.
437        let mut g = Graph::with_vertices(3);
438        g.add_edge(0, 1).unwrap();
439        g.add_edge(1, 2).unwrap();
440        assert!(eulerian_cycle(&g).is_err());
441    }
442
443    #[test]
444    fn cycle_k4_errors() {
445        // K4: 4 odd-degree vertices → no Euler path or cycle.
446        let mut g = Graph::with_vertices(4);
447        for u in 0..4u32 {
448            for v in (u + 1)..4 {
449                g.add_edge(u, v).unwrap();
450            }
451        }
452        assert!(eulerian_cycle(&g).is_err());
453    }
454
455    #[test]
456    fn cycle_directed_3cycle() {
457        let mut g = Graph::new(3, true).unwrap();
458        g.add_edge(0, 1).unwrap();
459        g.add_edge(1, 2).unwrap();
460        g.add_edge(2, 0).unwrap();
461        let cycle = eulerian_cycle(&g).unwrap();
462        assert_eq!(cycle.len(), 3);
463    }
464
465    #[test]
466    fn cycle_directed_path_errors() {
467        // 0->1->2: directed Euler path but no cycle.
468        let mut g = Graph::new(3, true).unwrap();
469        g.add_edge(0, 1).unwrap();
470        g.add_edge(1, 2).unwrap();
471        assert!(eulerian_cycle(&g).is_err());
472    }
473
474    #[test]
475    fn complex_eulerian_path_test_eulerian_r() {
476        // R test-eulerian.R 6-vertex literal-graph case has Euler path but no cycle.
477        // Edges: 0-1, 1-2, 2-3, 3-4, 4-0, 0-5, 5-3, 3-1, 1-5, 5-4 (10 edges).
478        let mut g = Graph::with_vertices(6);
479        for &(u, v) in &[
480            (0, 1),
481            (1, 2),
482            (2, 3),
483            (3, 4),
484            (4, 0),
485            (0, 5),
486            (5, 3),
487            (3, 1),
488            (1, 5),
489            (5, 4),
490        ] {
491            g.add_edge(u, v).unwrap();
492        }
493        let walk = eulerian_path(&g).unwrap().unwrap();
494        assert_eq!(walk.len(), 10, "must visit every edge exactly once");
495        assert!(walk_validates(&g, &walk));
496    }
497}