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 path or cycle for `graph` if one exists.
17/// Returns `Some(edge_ids)` (the walk visits each edge exactly once)
18/// or `None` if no Eulerian walk exists.
19///
20/// Counterpart of `igraph_eulerian_path()` (returns the path if any)
21/// from `references/igraph/src/paths/eulerian.c:345`. Undirected only
22/// in this slice; directed graphs return an error.
23///
24/// # Examples
25///
26/// ```
27/// use rust_igraph::{Graph, eulerian_path};
28///
29/// // Triangle 0-1-2-0: every vertex has even degree → Euler cycle exists.
30/// let mut g = Graph::with_vertices(3);
31/// g.add_edge(0, 1).unwrap();   // edge 0
32/// g.add_edge(1, 2).unwrap();   // edge 1
33/// g.add_edge(2, 0).unwrap();   // edge 2
34/// let walk = eulerian_path(&g).unwrap().unwrap();
35/// assert_eq!(walk.len(), 3);
36///
37/// // Path 0-1-2: 2 odd-degree vertices → Euler path (no cycle).
38/// let mut g = Graph::with_vertices(3);
39/// g.add_edge(0, 1).unwrap();
40/// g.add_edge(1, 2).unwrap();
41/// let walk = eulerian_path(&g).unwrap().unwrap();
42/// assert_eq!(walk.len(), 2);
43///
44/// // K4: 4 odd-degree vertices → no Euler path.
45/// let mut g = Graph::with_vertices(4);
46/// for u in 0..4u32 {
47///     for v in (u + 1)..4 {
48///         g.add_edge(u, v).unwrap();
49///     }
50/// }
51/// assert_eq!(eulerian_path(&g).unwrap(), None);
52/// ```
53pub fn eulerian_path(graph: &Graph) -> IgraphResult<Option<Vec<EdgeId>>> {
54    let cls = is_eulerian(graph)?;
55    if !cls.has_path {
56        return Ok(None);
57    }
58
59    let n = graph.vcount();
60    let m = graph.ecount();
61    if m == 0 {
62        // Empty walk for graphs with no edges (still trivially Eulerian).
63        return Ok(Some(Vec::new()));
64    }
65
66    let n_us = n as usize;
67    let m_us = m;
68    let directed = graph.is_directed();
69
70    // Per-vertex incident-edge lists. For undirected graphs `incident()`
71    // returns LOOPS_TWICE (self-loops appear twice); upstream's Hierholzer
72    // uses LOOPS_ONCE so we dedupe. For directed graphs we use the `oi`-side
73    // out-edges directly via `incident()`'s directed branch (same semantics
74    // as upstream's `IGRAPH_OUT` mode).
75    let mut inc: Vec<Vec<EdgeId>> = Vec::with_capacity(n_us);
76    for v in 0..n {
77        let raw = graph.incident(v)?;
78        if directed {
79            // Already out-only and one entry per edge.
80            inc.push(raw);
81        } else {
82            let mut seen: std::collections::HashSet<EdgeId> = std::collections::HashSet::new();
83            let mut out: Vec<EdgeId> = Vec::with_capacity(raw.len());
84            for e in raw {
85                if seen.insert(e) {
86                    out.push(e);
87                }
88            }
89            inc.push(out);
90        }
91    }
92
93    // Track simple "remaining degree" per vertex via the count of unvisited
94    // incident edges. Upstream uses `igraph_degree(_, IGRAPH_LOOPS)` which
95    // counts self-loops twice; we use the pre-built inc list and the visited
96    // bitset.
97    let mut visited: Vec<bool> = vec![false; m_us];
98    let mut next_idx: Vec<usize> = vec![0; n_us];
99
100    // Pick the start vertex: per upstream's logic in is_eulerian helpers,
101    // it's an odd-degree vertex if `has_path && !has_cycle`, otherwise
102    // any vertex with a non-zero unvisited incident edge.
103    let start_of_path = pick_start_vertex(graph, cls)?;
104
105    // Hierholzer's algorithm (iterative). Two stacks: `tracker` is the
106    // current walk; `path` is the output (built in reverse).
107    let mut tracker: Vec<VertexId> = Vec::with_capacity(n_us);
108    let mut edge_tracker: Vec<EdgeId> = Vec::with_capacity(m_us);
109    let mut path: Vec<VertexId> = Vec::with_capacity(n_us);
110    let mut edge_path: Vec<EdgeId> = Vec::with_capacity(m_us);
111
112    tracker.push(start_of_path);
113    let mut curr = start_of_path;
114
115    loop {
116        // Advance through `curr`'s next unvisited incident edge, if any.
117        let curr_us = curr as usize;
118        // Skip already-visited edges in the per-vertex iterator.
119        while next_idx[curr_us] < inc[curr_us].len()
120            && visited[inc[curr_us][next_idx[curr_us]] as usize]
121        {
122            next_idx[curr_us] += 1;
123        }
124        if next_idx[curr_us] < inc[curr_us].len() {
125            let edge = inc[curr_us][next_idx[curr_us]];
126            visited[edge as usize] = true;
127            next_idx[curr_us] += 1;
128            tracker.push(curr);
129            edge_tracker.push(edge);
130            curr = if directed {
131                // Directed: edge always points from curr → to[edge].
132                graph.edge_target(edge)?
133            } else {
134                graph.edge_other(edge, curr)?
135            };
136        } else {
137            // Dead end at `curr`: pop the walk.
138            path.push(curr);
139            if let Some(prev) = tracker.pop() {
140                if let Some(curr_e) = edge_tracker.pop() {
141                    edge_path.push(curr_e);
142                }
143                curr = prev;
144            } else {
145                break;
146            }
147        }
148    }
149
150    // edge_path was filled with the walk in reverse; reverse it now to get
151    // the forward edge order. (Upstream pops to the result vector, which
152    // also reverses the order.)
153    edge_path.reverse();
154    let _ = path; // vertex sequence — not part of the Phase-1 return
155
156    Ok(Some(edge_path))
157}
158
159fn pick_start_vertex(
160    graph: &Graph,
161    cls: crate::algorithms::paths::eulerian::EulerianClassification,
162) -> IgraphResult<VertexId> {
163    let n = graph.vcount();
164    let directed = graph.is_directed();
165    if cls.has_cycle {
166        // Any vertex with non-zero (out-)degree.
167        for v in 0..n {
168            let has_edges = if directed {
169                !graph.incident(v)?.is_empty()
170            } else {
171                !graph.neighbors(v)?.is_empty()
172            };
173            if has_edges {
174                return Ok(v);
175            }
176        }
177        // Should be unreachable since `m == 0` returns early above.
178        Err(IgraphError::Internal("no edges but cls.has_cycle"))
179    } else if directed {
180        // Directed path: pick the vertex with `out_degree - in_degree == 1`
181        // (the unique source per is_eulerian's outgoing_excess accounting).
182        for v in 0..n {
183            let out_deg = graph.incident(v)?.len();
184            // in-degree via the in-side
185            let in_deg = compute_in_degree(graph, v)?;
186            if out_deg > in_deg && (out_deg - in_deg) == 1 {
187                return Ok(v);
188            }
189        }
190        Err(IgraphError::Internal(
191            "directed path: no vertex with outgoing_excess == 1",
192        ))
193    } else {
194        // Undirected path: smallest-id odd-degree vertex.
195        for v in 0..n {
196            let deg = graph.degree(v)?;
197            if deg % 2 != 0 {
198                return Ok(v);
199            }
200        }
201        Err(IgraphError::Internal(
202            "has_path && !has_cycle but no odd-degree vertex found",
203        ))
204    }
205}
206
207fn compute_in_degree(graph: &Graph, v: VertexId) -> IgraphResult<usize> {
208    // Total degree via undirected `degree` would mix in/out; use a fresh
209    // count via in-side inferred from edges (n times m worst case is fine
210    // here — only called once per vertex during start selection).
211    let mut count = 0usize;
212    let m = u32::try_from(graph.ecount()).expect("edge count fits in u32");
213    for e in 0..m {
214        if graph.edge_target(e)? == v {
215            count += 1;
216        }
217    }
218    Ok(count)
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    fn walk_validates(graph: &Graph, walk: &[EdgeId]) -> bool {
226        // Each edge appears exactly once.
227        let mut seen: Vec<bool> = vec![false; graph.ecount()];
228        for &edge in walk {
229            let idx = edge as usize;
230            if idx >= graph.ecount() || seen[idx] {
231                return false;
232            }
233            seen[idx] = true;
234        }
235        // Walk is consecutively connected.
236        if walk.len() < 2 {
237            return true;
238        }
239        for i in 0..walk.len() - 1 {
240            let (a, b) = graph.edge(walk[i]).unwrap();
241            let (c, d) = graph.edge(walk[i + 1]).unwrap();
242            if !(a == c || a == d || b == c || b == d) {
243                return false;
244            }
245        }
246        true
247    }
248
249    #[test]
250    fn empty_graph_returns_empty_walk() {
251        let g = Graph::with_vertices(0);
252        assert_eq!(eulerian_path(&g).unwrap(), Some(Vec::new()));
253    }
254
255    #[test]
256    fn isolated_vertices_return_empty_walk() {
257        let g = Graph::with_vertices(5);
258        assert_eq!(eulerian_path(&g).unwrap(), Some(Vec::new()));
259    }
260
261    #[test]
262    fn triangle_yields_three_edge_walk() {
263        let mut g = Graph::with_vertices(3);
264        g.add_edge(0, 1).unwrap();
265        g.add_edge(1, 2).unwrap();
266        g.add_edge(2, 0).unwrap();
267        let walk = eulerian_path(&g).unwrap().unwrap();
268        assert_eq!(walk.len(), 3);
269        assert!(walk_validates(&g, &walk));
270    }
271
272    #[test]
273    fn path_3_yields_two_edge_walk() {
274        let mut g = Graph::with_vertices(3);
275        g.add_edge(0, 1).unwrap();
276        g.add_edge(1, 2).unwrap();
277        let walk = eulerian_path(&g).unwrap().unwrap();
278        assert_eq!(walk.len(), 2);
279        assert!(walk_validates(&g, &walk));
280    }
281
282    #[test]
283    fn k4_has_no_eulerian_walk() {
284        let mut g = Graph::with_vertices(4);
285        for u in 0..4u32 {
286            for v in (u + 1)..4 {
287                g.add_edge(u, v).unwrap();
288            }
289        }
290        assert_eq!(eulerian_path(&g).unwrap(), None);
291    }
292
293    #[test]
294    fn disconnected_components_no_walk() {
295        let mut g = Graph::with_vertices(4);
296        g.add_edge(0, 1).unwrap();
297        g.add_edge(2, 3).unwrap();
298        assert_eq!(eulerian_path(&g).unwrap(), None);
299    }
300
301    #[test]
302    fn ring_5_walk_visits_all_edges() {
303        let mut g = Graph::with_vertices(5);
304        for i in 0..5u32 {
305            g.add_edge(i, (i + 1) % 5).unwrap();
306        }
307        let walk = eulerian_path(&g).unwrap().unwrap();
308        assert_eq!(walk.len(), 5);
309        assert!(walk_validates(&g, &walk));
310    }
311
312    #[test]
313    fn directed_3_cycle_yields_3_edge_walk() {
314        // 0 -> 1 -> 2 -> 0: directed cycle, has Eulerian cycle.
315        let mut g = Graph::new(3, true).unwrap();
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    }
322
323    #[test]
324    fn directed_path_yields_chain_walk() {
325        // 0 -> 1 -> 2: directed Eulerian path (out_excess at 0 = 1).
326        let mut g = Graph::new(3, true).unwrap();
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, vec![0, 1]); // edges traversed in order
331    }
332
333    #[test]
334    fn directed_no_eulerian_returns_none() {
335        // 0 -> 1 -> 2 plus 0 -> 2: two out-edges from 0, two in to 2 → no path.
336        let mut g = Graph::new(3, true).unwrap();
337        g.add_edge(0, 1).unwrap();
338        g.add_edge(1, 2).unwrap();
339        g.add_edge(0, 2).unwrap();
340        assert_eq!(eulerian_path(&g).unwrap(), None);
341    }
342
343    #[test]
344    fn complex_eulerian_path_test_eulerian_r() {
345        // R test-eulerian.R 6-vertex literal-graph case has Euler path but no cycle.
346        // Edges: 0-1, 1-2, 2-3, 3-4, 4-0, 0-5, 5-3, 3-1, 1-5, 5-4 (10 edges).
347        let mut g = Graph::with_vertices(6);
348        for &(u, v) in &[
349            (0, 1),
350            (1, 2),
351            (2, 3),
352            (3, 4),
353            (4, 0),
354            (0, 5),
355            (5, 3),
356            (3, 1),
357            (1, 5),
358            (5, 4),
359        ] {
360            g.add_edge(u, v).unwrap();
361        }
362        let walk = eulerian_path(&g).unwrap().unwrap();
363        assert_eq!(walk.len(), 10, "must visit every edge exactly once");
364        assert!(walk_validates(&g, &walk));
365    }
366}