graph_advanced/euler.rs
1use graph_core::{Graph, NodeId};
2use std::collections::HashMap;
3
4/// Errors returned by Euler path/circuit algorithms.
5#[derive(Debug, Clone, PartialEq)]
6pub enum EulerError {
7 /// The graph has no nodes.
8 EmptyGraph,
9 /// The graph is disconnected (ignoring isolated nodes), so no Euler
10 /// path or circuit can traverse all edges.
11 Disconnected,
12 /// The degree conditions for an Euler circuit are not met.
13 ///
14 /// For undirected graphs: all nodes must have even degree.
15 /// For directed graphs: every node must have equal in-degree and out-degree.
16 NoCircuit,
17 /// The degree conditions for an Euler path are not met.
18 ///
19 /// For undirected graphs: exactly two nodes must have odd degree.
20 /// For directed graphs: exactly one node must have `out - in = 1` (start)
21 /// and one must have `in - out = 1` (end).
22 NoPath,
23}
24
25impl std::fmt::Display for EulerError {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 match self {
28 EulerError::EmptyGraph => write!(f, "graph is empty"),
29 EulerError::Disconnected => write!(f, "graph is disconnected"),
30 EulerError::NoCircuit => write!(f, "Euler circuit does not exist (odd-degree nodes)"),
31 EulerError::NoPath => write!(f, "Euler path does not exist (wrong degree sequence)"),
32 }
33 }
34}
35
36impl std::error::Error for EulerError {}
37
38/// Finds an **Euler circuit** in an undirected graph using Hierholzer's algorithm.
39///
40/// An Euler circuit visits every edge **exactly once** and returns to the
41/// starting node. It exists iff the graph is connected (ignoring isolated
42/// nodes) and every node has even degree.
43///
44/// # Returns
45///
46/// `Ok(Vec<NodeId>)` — the sequence of nodes visited, with the first node
47/// repeated at the end (first == last).
48///
49/// # Errors
50///
51/// - [`EulerError::EmptyGraph`] — no nodes in the graph.
52/// - [`EulerError::Disconnected`] — graph is not connected.
53/// - [`EulerError::NoCircuit`] — some node has odd degree.
54///
55/// # Complexity
56///
57/// O(E) — Hierholzer's algorithm visits each edge exactly twice
58/// (once per direction in the undirected adjacency list).
59///
60/// # Examples
61///
62/// ```
63/// use graph_core::{AdjacencyList, Graph};
64/// use graph_advanced::euler_circuit;
65///
66/// // Complete graph K3: triangle — Euler circuit exists.
67/// let mut g: AdjacencyList<()> = AdjacencyList::undirected();
68/// let n: Vec<_> = (0..3).map(|_| g.add_node(())).collect();
69/// g.add_edge(n[0], n[1], 1.0).unwrap();
70/// g.add_edge(n[1], n[2], 1.0).unwrap();
71/// g.add_edge(n[2], n[0], 1.0).unwrap();
72///
73/// let circuit = euler_circuit(&g).unwrap();
74/// assert_eq!(circuit.len(), 4); // 3 edges + return to start
75/// assert_eq!(circuit.first(), circuit.last());
76/// ```
77pub fn euler_circuit<G>(graph: &G) -> Result<Vec<NodeId>, EulerError>
78where
79 G: Graph<Weight = f64>,
80{
81 let start = first_non_isolated_node(graph).ok_or(EulerError::EmptyGraph)?;
82
83 // Check: all nodes with edges must have even degree.
84 for node in graph.nodes() {
85 if graph.degree(node) % 2 != 0 {
86 return Err(EulerError::NoCircuit);
87 }
88 }
89
90 let circuit = hierholzer(graph, start)?;
91 Ok(circuit)
92}
93
94/// Finds an **Euler path** in an undirected graph using Hierholzer's algorithm.
95///
96/// An Euler path visits every edge **exactly once** but need not return to the
97/// start. It exists iff the graph is connected (ignoring isolated nodes) and
98/// **exactly two** nodes have odd degree (these are the path endpoints).
99///
100/// # Returns
101///
102/// `Ok(Vec<NodeId>)` — the sequence of nodes visited, from the odd-degree
103/// start node to the other odd-degree end node.
104///
105/// # Errors
106///
107/// - [`EulerError::EmptyGraph`] — no nodes in the graph.
108/// - [`EulerError::Disconnected`] — graph is not connected.
109/// - [`EulerError::NoPath`] — the graph does not have exactly two odd-degree nodes.
110///
111/// # Complexity
112///
113/// O(E).
114///
115/// # Examples
116///
117/// ```
118/// use graph_core::{AdjacencyList, Graph};
119/// use graph_advanced::euler_path;
120///
121/// // Path graph 0-1-2-3: nodes 0 and 3 have odd degree (1 each).
122/// let mut g: AdjacencyList<()> = AdjacencyList::undirected();
123/// let n: Vec<_> = (0..4).map(|_| g.add_node(())).collect();
124/// g.add_edge(n[0], n[1], 1.0).unwrap();
125/// g.add_edge(n[1], n[2], 1.0).unwrap();
126/// g.add_edge(n[2], n[3], 1.0).unwrap();
127///
128/// let path = euler_path(&g).unwrap();
129/// assert_eq!(path.len(), 4); // 3 edges → 4 nodes
130/// ```
131pub fn euler_path<G>(graph: &G) -> Result<Vec<NodeId>, EulerError>
132where
133 G: Graph<Weight = f64>,
134{
135 if graph.node_count() == 0 {
136 return Err(EulerError::EmptyGraph);
137 }
138
139 let odd_nodes: Vec<NodeId> = graph
140 .nodes()
141 .filter(|&n| graph.degree(n) % 2 != 0)
142 .collect();
143
144 if odd_nodes.len() != 2 {
145 return Err(EulerError::NoPath);
146 }
147
148 // Start from one of the two odd-degree nodes.
149 let start = odd_nodes[0];
150 let circuit = hierholzer(graph, start)?;
151 Ok(circuit)
152}
153
154// ── Internal helpers ──────────────────────────────────────────────────────────
155
156/// Hierholzer's algorithm: finds an Euler circuit/path starting at `start`.
157///
158/// Builds a local adjacency list from the graph and tracks which neighbour
159/// slots have been consumed via a per-node pointer. For undirected graphs
160/// the `Graph` trait stores each edge in **both** directions; to avoid
161/// traversing the same logical edge twice we assign each adjacency-list entry
162/// a unique global edge id and mark it used when traversed.
163///
164/// Uses an explicit stack instead of recursion to avoid stack overflow.
165fn hierholzer<G>(graph: &G, start: NodeId) -> Result<Vec<NodeId>, EulerError>
166where
167 G: Graph<Weight = f64>,
168{
169 let nodes: Vec<NodeId> = graph.nodes().collect();
170 let n = nodes.len();
171 let index_of: HashMap<NodeId, usize> =
172 nodes.iter().enumerate().map(|(i, &id)| (id, i)).collect();
173
174 // Build adjacency as (neighbour_node_index, edge_id) pairs.
175 // For an undirected graph each logical edge (u,v) appears as two
176 // adjacency-list entries. We assign consecutive ids to neighbours of each
177 // node in order, then identify the paired entry by matching neighbours
178 // across both directions. Instead we use a simpler scheme: give each
179 // directed adjacency-list slot a unique id, and store both directions with
180 // matching ids so we can mark the reverse as used too.
181 //
182 // Scheme: for each node u enumerate its neighbours in order. The forward
183 // entry (u, i) gets edge_id = base_offset[u] + i. We then build a
184 // reverse lookup: reverse[v][edge_id_of_u_to_v] = edge_id_of_v_to_u.
185
186 // adj[i] = list of (neighbour_index, edge_id)
187 let mut adj: Vec<Vec<(usize, usize)>> = vec![Vec::new(); n];
188 let mut global_id = 0usize;
189 let mut edge_id_base: Vec<usize> = vec![0; n]; // starting edge_id for each node's adj list
190
191 for (ui, &u) in nodes.iter().enumerate() {
192 edge_id_base[ui] = global_id;
193 for (v, _) in graph.neighbors(u) {
194 let vi = index_of[&v];
195 adj[ui].push((vi, global_id));
196 global_id += 1;
197 }
198 }
199
200 // Total directed adjacency entries = global_id.
201 let mut used = vec![false; global_id];
202
203 // For each directed entry (u→v with edge_id e_uv), find the corresponding
204 // reverse entry (v→u with edge_id e_vu) so we can mark both used at once.
205 // We do this by building a map: (u_idx, v_idx) → list of edge_ids from u to v.
206 // Then for each (u, v, e_uv) the reverse is any unused edge_id in (v, u).
207 // To keep it O(E), pre-build reverse_id[edge_id] = paired reverse edge_id.
208 //
209 // For a directed graph there is no paired reverse entry — we only mark the
210 // forward direction. We detect undirected mode by checking whether
211 // every adjacency entry has a matching reverse.
212
213 // reverse_of[e] = edge_id of the reverse half-edge, or usize::MAX if none.
214 let mut reverse_of = vec![usize::MAX; global_id];
215 {
216 // For each node v, build a map neighbour_idx → queue of edge_ids coming IN to v (i.e. the v→u edges).
217 // We use these to pair up undirected half-edges.
218 let mut in_edges: Vec<HashMap<usize, std::collections::VecDeque<usize>>> =
219 vec![HashMap::new(); n];
220 for (ui, adj_ui) in adj.iter().enumerate() {
221 for &(vi, eid) in adj_ui {
222 in_edges[vi].entry(ui).or_default().push_back(eid);
223 }
224 }
225 // For each directed entry u→v (edge_id e_uv), look in in_edges[u] for a v→u edge.
226 for (ui, adj_ui) in adj.iter().enumerate() {
227 for &(vi, e_uv) in adj_ui {
228 if let Some(queue) = in_edges[ui].get_mut(&vi) {
229 if let Some(e_vu) = queue.pop_front() {
230 reverse_of[e_uv] = e_vu;
231 }
232 }
233 }
234 }
235 }
236
237 // Count logical edges to use for the final length check.
238 // For undirected graphs (where reverse_of[e] != MAX), each logical edge
239 // is counted twice in global_id, so we divide by 2 for the check.
240 // We infer undirected mode if at least one reverse pair exists.
241 let has_reverse = reverse_of.iter().any(|&r| r != usize::MAX);
242 let logical_edge_count = if has_reverse {
243 global_id / 2
244 } else {
245 global_id
246 };
247
248 // Hierholzer's with per-node pointer (advance past used edges).
249 let mut ptr: Vec<usize> = vec![0; n];
250 let start_idx = index_of[&start];
251 let mut stack: Vec<usize> = vec![start_idx];
252 let mut circuit: Vec<usize> = Vec::new();
253
254 while let Some(&curr) = stack.last() {
255 // Advance ptr past any already-used edges.
256 while ptr[curr] < adj[curr].len() && used[adj[curr][ptr[curr]].1] {
257 ptr[curr] += 1;
258 }
259
260 if ptr[curr] < adj[curr].len() {
261 let (next, eid) = adj[curr][ptr[curr]];
262 ptr[curr] += 1;
263 // Mark this edge (and its reverse) as used.
264 used[eid] = true;
265 if reverse_of[eid] != usize::MAX {
266 used[reverse_of[eid]] = true;
267 }
268 stack.push(next);
269 } else {
270 circuit.push(stack.pop().unwrap());
271 }
272 }
273
274 circuit.reverse();
275
276 // Check that all edges were consumed (connectivity check).
277 if circuit.len() != logical_edge_count + 1 {
278 return Err(EulerError::Disconnected);
279 }
280
281 // Convert indices back to NodeIds.
282 Ok(circuit.into_iter().map(|i| nodes[i]).collect())
283}
284
285/// Returns the first node that has at least one edge, or `None` if the graph
286/// is empty or all nodes are isolated.
287fn first_non_isolated_node<G>(graph: &G) -> Option<NodeId>
288where
289 G: Graph,
290{
291 graph.nodes().find(|&n| graph.degree(n) > 0)
292}