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}