Euler Path and Circuit
Key Points
- ā¢An Euler path visits every edge exactly once, and an Euler circuit is an Euler path that starts and ends at the same vertex.
- ā¢In an undirected graph, an Euler circuit exists if and only if the graph is connected (ignoring isolated vertices) and every vertex has even degree.
- ā¢In an undirected graph, an Euler path (but not a circuit) exists if and only if the graph is connected and exactly two vertices have odd degree.
- ā¢Hierholzerās algorithm constructs an Euler path or circuit in O(E) time by stitching together edge-disjoint cycles.
- ā¢Connectivity must be checked on the subgraph induced by vertices of nonzero degree; isolated vertices do not block Euler trails.
- ā¢For directed graphs, a circuit exists when every vertex has equal in-degree and out-degree and the graph is weakly connected.
- ā¢For directed graphs, a path (not circuit) exists when exactly one vertex has out-degree one more than in-degree (start) and one vertex has in-degree one more than out-degree (end).
- ā¢Using a multiset per vertex allows computing the lexicographically smallest Euler trail, typically in O(E log E) time.
Prerequisites
- āGraphs and basic terminology ā Understanding vertices, edges, degree, and connectivity is essential to state Euler conditions.
- āDepth-first search (DFS) / Breadth-first search (BFS) ā Connectivity checks and traversal intuition rely on standard graph search methods.
- āStacks and recursion ā Hierholzerās algorithm is conveniently implemented with a stack or recursive calls.
- āAdjacency list representation ā Efficient storage and iteration over edges is required for O(E) implementations.
- āMultisets / priority queues ā Needed to produce lexicographically smallest Euler trails.
- āBig-O notation ā To analyze the linear-time complexity and space usage of the algorithm.
Detailed Explanation
Tap terms for definitions01Overview
Euler paths and circuits are special traversals in graphs that use each edge exactly once. An Euler path may start and end at different vertices, while an Euler circuit (also called an Eulerian cycle) starts and ends at the same vertex. The central question is: when do these traversals exist, and how can we construct them efficiently? Eulerās classical results give crisp degree-based conditions: in undirected graphs, all vertices must have even degree for a circuit, and exactly two vertices may be odd for a path. For directed graphs, equal in- and out-degree at each vertex yields a circuit; having exactly one vertex with out-degree exceeding in-degree by one (start) and one with the opposite (end) yields a path. The standard linear-time construction method is Hierholzerās algorithm, which finds small cycles and splices them together to cover all edges without repetition. This topic is widely applicable in problems that model streets and trails, drawing figures without lifting a pen, and assembling sequences from overlaps.
02Intuition & Analogies
Imagine you are a postal worker who wants to traverse every street in a neighborhood exactly once. If every intersection has an even number of streets entering it, you can always pair the incoming and outgoing streets so you never get stuck; that makes a perfect loop you can start and end at the same placeāan Euler circuit. If exactly two intersections have an odd number of streets, think of them as the unavoidable entry and exit pointsāyou must start at one odd intersection and finish at the other to balance the unpaired street. If there are more than two odd intersections, eventually you will get stuck at some point with no unused street to leave on. Hierholzerās algorithm mirrors how you might physically walk: start anywhere and keep following unused streets until you return to your start, forming a small loop. If any unused streets remain elsewhere, jump to a vertex on your loop that still has unused streets and grow another loop from there. Keep splicing these loops into one big itinerary. For directed graphs, think of one-way streets: to be able to flow through the entire city without getting stuck, the number of incoming and outgoing one-way streets at every intersection must balance, except possibly at the start and end where you have one extra outgoing and one extra incoming, respectively.
03Formal Definition
04When to Use
Use Euler paths and circuits when the task is to traverse all edges exactly once. Classic examples include street-sweeping or postal routes where each road must be covered once, drawing puzzles like the Seven Bridges of Kƶnigsberg, and reconstructing sequences from overlaps (e.g., Eulerian paths in de Bruijn graphs for genome assembly). In competitive programming, Euler trails appear in problems about reordering strings by matching first/last letters, verifying possible itineraries over one-way flights, and finding tours in multigraphs with parallel edges and self-loops. Choose Hierholzerās algorithm when E is large; it runs in O(E) and is simple to implement with a stack and edge-usage flags. If the output should be lexicographically smallest, adapt the traversal to always take the smallest available edge (using multisets or priority queues), trading speed for ordering. Ensure you first check degree conditions and connectivity on the subgraph induced by nonzero-degree vertices; otherwise you may chase non-existent tours.
ā ļøCommon Mistakes
Common pitfalls include: (1) Checking connectivity on the whole graph instead of the subgraph with nonzero-degree vertices; isolated vertices should be ignored. (2) Forgetting that an undirected Euler path requires exactly 0 or 2 odd-degree vertices; more than two means impossible. (3) In directed graphs, mixing up the start/end rules; the start must have out-degree = in-degree + 1 and the end must have in-degree = out-degree + 1, with all others balanced. (4) Not marking each undirected edge as used exactly once for both endpoints; using a single boolean per edge ID is safer than erasing from only one side. (5) Implementing a naive DFS that backtracks arbitrarily; this can get stuck even when a trail exists. Hierholzerās algorithm avoids getting stuck by deferring dead-ends to when no unused edges leave the current vertex. (6) Ignoring self-loops and parallel edges; be sure your adjacency structure supports duplicates and that self-loops are counted twice toward degree in undirected graphs. (7) Recursion depth limits; for very large E, prefer an iterative stack-based implementation to avoid stack overflow. (8) Assuming lexicographically smallest Euler tour comes for free; you must use ordered adjacency (multiset or priority queue) and accept an O(E log E) time bound.
Key Formulas
Degree Sum Formula (Undirected)
Explanation: The total of all vertex degrees equals twice the number of edges because each edge contributes 2 to the sum. This implies the number of odd-degree vertices is even.
Undirected Euler Condition (Parity)
Explanation: An undirected graph has an Euler circuit when this count is 0, and an Euler path when this count is 2, assuming connectivity on the nonzero-degree subgraph.
Directed Euler Circuit Degree Balance
Explanation: A directed Euler circuit exists only if every vertex has equal in-degree and out-degree, and the graph is weakly connected on nonzero-degree vertices.
Directed Euler Path Degree Imbalance
Explanation: A directed Euler path (but not circuit) exists when exactly one start vertex has one extra outgoing edge and exactly one end vertex has one extra incoming edge; all others are balanced.
Hierholzerās Time Complexity
Explanation: Each edge is traversed a constant number of times (pushed and popped once), yielding linear time in the number of edges.
Trail Length
Explanation: An Euler trail of E edges always visits exactly E+1 vertices counting repetitions along the walk. This is a useful correctness check after construction.
Connectivity Condition (Undirected)
Explanation: You must check that all vertices with nonzero degree lie in one connected component. Isolated vertices do not affect the existence of an Euler trail.
Complexity Analysis
Code Examples
1 #include <iostream> 2 #include <vector> 3 #include <stack> 4 #include <algorithm> 5 6 // Undirected Euler path/circuit using Hierholzer's algorithm. 7 // - Works for multigraphs and self-loops 8 // - Checks existence conditions and connectivity (ignoring isolated vertices) 9 10 struct UndirectedEuler { 11 int n; // number of vertices (0-indexed) 12 std::vector<std::vector<std::pair<int,int>>> adj; // adj[u] = list of (v, edge_id) 13 std::vector<int> deg; // degree of each vertex 14 std::vector<int> U, V; // endpoints per edge id 15 std::vector<char> used; // used[edge_id] 16 17 UndirectedEuler(int n): n(n), adj(n), deg(n, 0) {} 18 19 int add_edge(int u, int v) { 20 int id = (int)U.size(); 21 U.push_back(u); V.push_back(v); 22 if ((int)used.size() <= id) used.resize(id + 1, 0); 23 adj[u].push_back({v, id}); 24 adj[v].push_back({u, id}); 25 if (u == v) { 26 // self-loop contributes 2 to degree 27 deg[u] += 2; 28 } else { 29 deg[u]++; deg[v]++; 30 } 31 return id; 32 } 33 34 // Check connectivity on the subgraph induced by vertices with deg > 0 35 bool connected_ignoring_isolates(int start) { 36 if (start == -1) return true; // no edges at all 37 std::vector<char> vis(n, 0); 38 std::stack<int> st; st.push(start); vis[start] = 1; 39 while (!st.empty()) { 40 int u = st.top(); st.pop(); 41 for (auto [v, id] : adj[u]) { 42 if (!vis[v]) { vis[v] = 1; st.push(v); } 43 } 44 } 45 for (int v = 0; v < n; ++v) if (deg[v] > 0 && !vis[v]) return false; 46 return true; 47 } 48 49 // Returns empty vector if no Euler trail exists. 50 // Otherwise returns the vertex sequence (size = E + 1). 51 std::vector<int> euler_trail_or_circuit() { 52 int E = (int)U.size(); 53 used.assign(E, 0); 54 55 // Pick start vertex based on odd-degree count 56 std::vector<int> odd; 57 for (int v = 0; v < n; ++v) if (deg[v] % 2 == 1) odd.push_back(v); 58 if (!(odd.size() == 0 || odd.size() == 2)) return {}; // necessary condition fails 59 60 int start = -1; 61 if (odd.size() == 2) start = odd[0]; 62 else { 63 for (int v = 0; v < n; ++v) if (deg[v] > 0) { start = v; break; } 64 } 65 if (!connected_ignoring_isolates(start)) return {}; 66 if (start == -1) return {0}; // Graph with no edges; define trivial path 67 68 std::vector<int> it(n, 0); // next index to scan per vertex 69 std::vector<int> out; out.reserve(E + 1); 70 std::stack<int> st; st.push(start); 71 72 while (!st.empty()) { 73 int u = st.top(); 74 // advance iterator to the next unused edge 75 while (it[u] < (int)adj[u].size() && used[ adj[u][it[u]].second ]) it[u]++; 76 if (it[u] == (int)adj[u].size()) { 77 // no more edges from u: add to path 78 out.push_back(u); 79 st.pop(); 80 } else { 81 auto [v, id] = adj[u][it[u]++]; 82 if (used[id]) continue; // skip already used (seen from the other side) 83 used[id] = 1; 84 st.push(v); 85 } 86 } 87 if ((int)out.size() != E + 1) return {}; // not all edges were used 88 std::reverse(out.begin(), out.end()); 89 return out; 90 } 91 }; 92 93 int main() { 94 // Example: undirected graph with an Euler path 95 // 0-1-2-0 and 1-3 (odd vertices are 1 and 3) 96 UndirectedEuler G(4); 97 G.add_edge(0,1); 98 G.add_edge(1,2); 99 G.add_edge(2,0); 100 G.add_edge(1,3); 101 102 auto trail = G.euler_trail_or_circuit(); 103 if (trail.empty()) { 104 std::cout << "No Euler trail\n"; 105 } else { 106 std::cout << "Euler trail (vertices): "; 107 for (size_t i = 0; i < trail.size(); ++i) { 108 if (i) std::cout << ' '; 109 std::cout << trail[i]; 110 } 111 std::cout << "\nLength (edges) = " << (int)trail.size() - 1 << "\n"; 112 } 113 return 0; 114 } 115
This program constructs an undirected multigraph and finds an Euler path or circuit if it exists. It checks degree parity and connectivity on the subgraph with nonzero degree. Hierholzerās algorithm uses a stack and a per-vertex scan pointer to traverse each edge exactly once and build the final vertex sequence. Edge IDs ensure each undirected edge is used only once.
1 #include <iostream> 2 #include <vector> 3 #include <stack> 4 #include <algorithm> 5 6 struct DirectedEuler { 7 int n; 8 std::vector<std::vector<std::pair<int,int>>> adj; // adj[u] = (v, id) 9 std::vector<int> indeg, outdeg; 10 std::vector<int> U, V; // edges u->v 11 std::vector<char> used; 12 13 DirectedEuler(int n): n(n), adj(n), indeg(n,0), outdeg(n,0) {} 14 15 int add_edge(int u, int v) { 16 int id = (int)U.size(); 17 U.push_back(u); V.push_back(v); 18 if ((int)used.size() <= id) used.resize(id + 1, 0); 19 adj[u].push_back({v, id}); 20 outdeg[u]++; indeg[v]++; 21 return id; 22 } 23 24 // Weak connectivity on vertices with nonzero degree (ignore directions) 25 bool weakly_connected_ignoring_isolates(int start) { 26 if (start == -1) return true; // no edges 27 std::vector<char> vis(n, 0); 28 std::stack<int> st; st.push(start); vis[start] = 1; 29 while (!st.empty()) { 30 int u = st.top(); st.pop(); 31 // traverse outgoing 32 for (auto [v, id] : adj[u]) if (!vis[v]) { vis[v]=1; st.push(v); } 33 // traverse incoming by scanning all edges (build reverse view on the fly) 34 for (int v = 0; v < n; ++v) { 35 // Cheap but O(VE) if used naively; instead, we can rely on an auxiliary undirected adjacency. 36 } 37 } 38 // The loop above is inefficient; build an undirected adjacency once instead: 39 return false; 40 } 41 42 // Build weak connectivity using an auxiliary undirected adjacency (efficient) 43 bool weakly_connected() { 44 int start = -1; 45 std::vector<std::vector<int>> und(n); 46 for (size_t i = 0; i < U.size(); ++i) { 47 int a = U[i], b = V[i]; 48 und[a].push_back(b); 49 und[b].push_back(a); 50 } 51 for (int v = 0; v < n; ++v) if (indeg[v] + outdeg[v] > 0) { start = v; break; } 52 if (start == -1) return true; // no edges 53 std::vector<char> vis(n, 0); 54 std::stack<int> st; st.push(start); vis[start] = 1; 55 while (!st.empty()) { 56 int u = st.top(); st.pop(); 57 for (int w : und[u]) if (!vis[w]) { vis[w]=1; st.push(w); } 58 } 59 for (int v = 0; v < n; ++v) if ((indeg[v] + outdeg[v] > 0) && !vis[v]) return false; 60 return true; 61 } 62 63 // Returns empty if no Euler trail exists; otherwise vertex sequence of size E+1 64 std::vector<int> euler_trail_or_circuit() { 65 int E = (int)U.size(); 66 used.assign(E, 0); 67 68 if (!weakly_connected()) return {}; 69 70 int start = -1, end = -1; 71 for (int v = 0; v < n; ++v) { 72 int diff = outdeg[v] - indeg[v]; 73 if (diff == 1) { 74 if (start == -1) start = v; else return {}; 75 } else if (diff == -1) { 76 if (end == -1) end = v; else return {}; 77 } else if (diff != 0) { 78 return {}; // imbalance too large 79 } 80 } 81 if (start == -1) { 82 // Euler circuit case: start at any vertex with outdeg > 0 83 for (int v = 0; v < n; ++v) if (outdeg[v] > 0) { start = v; break; } 84 } 85 if (start == -1) return {0}; // empty graph: trivial path 86 87 std::vector<int> it(n, 0), out; 88 out.reserve(E + 1); 89 std::stack<int> st; st.push(start); 90 91 while (!st.empty()) { 92 int u = st.top(); 93 while (it[u] < (int)adj[u].size() && used[ adj[u][it[u]].second ]) it[u]++; 94 if (it[u] == (int)adj[u].size()) { 95 out.push_back(u); 96 st.pop(); 97 } else { 98 auto [v, id] = adj[u][it[u]++]; 99 if (used[id]) continue; 100 used[id] = 1; 101 st.push(v); 102 } 103 } 104 if ((int)out.size() != E + 1) return {}; 105 std::reverse(out.begin(), out.end()); 106 return out; 107 } 108 }; 109 110 int main() { 111 // Example: directed Euler circuit 112 // 0->1->2->0 and 0->3->1 (balances maintained) 113 DirectedEuler G(4); 114 G.add_edge(0,1); 115 G.add_edge(1,2); 116 G.add_edge(2,0); 117 G.add_edge(0,3); 118 G.add_edge(3,1); 119 120 auto path = G.euler_trail_or_circuit(); 121 if (path.empty()) { 122 std::cout << "No directed Euler trail\n"; 123 } else { 124 std::cout << "Directed Euler trail (vertices): "; 125 for (size_t i = 0; i < path.size(); ++i) { 126 if (i) std::cout << ' '; 127 std::cout << path[i]; 128 } 129 std::cout << "\nLength (edges) = " << (int)path.size() - 1 << "\n"; 130 } 131 return 0; 132 } 133
This program computes a directed Euler path or circuit. It checks the degree-imbalance conditions and weak connectivity on the underlying undirected graph. Hierholzerās algorithm proceeds identically to the undirected version, except each edge is used in one direction only.
1 #include <iostream> 2 #include <vector> 3 #include <set> 4 #include <algorithm> 5 6 // Compute lexicographically smallest Euler trail/circuit by always taking the smallest neighbor. 7 // Uses multiset adjacency; supports parallel edges and self-loops. 8 9 struct LexiEuler { 10 int n; 11 std::vector<std::multiset<int>> adj; // undirected multigraph 12 std::vector<int> deg; 13 14 LexiEuler(int n): n(n), adj(n), deg(n,0) {} 15 16 void add_edge(int u, int v) { 17 adj[u].insert(v); 18 adj[v].insert(u); 19 if (u == v) deg[u] += 2; else { deg[u]++; deg[v]++; } 20 } 21 22 bool connected_ignoring_isolates(int start) { 23 if (start == -1) return true; 24 std::vector<char> vis(n, 0); 25 std::vector<int> st = {start}; vis[start] = 1; 26 for (size_t i = 0; i < st.size(); ++i) { 27 int u = st[i]; 28 for (int v : adj[u]) if (!vis[v]) { vis[v] = 1; st.push_back(v); } 29 } 30 for (int v = 0; v < n; ++v) if (deg[v] > 0 && !vis[v]) return false; 31 return true; 32 } 33 34 void dfs(int u, std::vector<int>& out) { 35 while (!adj[u].empty()) { 36 int v = *adj[u].begin(); // smallest neighbor 37 // remove one copy from both sides 38 adj[u].erase(adj[u].begin()); 39 auto it = adj[v].find(u); 40 if (it == adj[v].end()) { 41 // should not happen in a well-formed undirected multigraph 42 continue; 43 } 44 adj[v].erase(it); 45 dfs(v, out); 46 } 47 out.push_back(u); 48 } 49 50 std::vector<int> smallest_euler() { 51 std::vector<int> odd; 52 for (int v = 0; v < n; ++v) if (deg[v] % 2 == 1) odd.push_back(v); 53 if (!(odd.size() == 0 || odd.size() == 2)) return {}; 54 int start = -1; 55 if (!odd.empty()) start = *std::min_element(odd.begin(), odd.end()); 56 else { 57 for (int v = 0; v < n; ++v) if (deg[v] > 0) { start = v; break; } 58 } 59 if (!connected_ignoring_isolates(start)) return {}; 60 if (start == -1) return {0}; // no edges 61 std::vector<int> out; 62 dfs(start, out); 63 std::reverse(out.begin(), out.end()); 64 // verify all edges used: sum deg / 2 edges implies out.size() == E + 1 65 long long E = 0; for (int d : deg) E += d; E /= 2; 66 if ((int)out.size() != (int)E + 1) return {}; // not all edges covered 67 return out; 68 } 69 }; 70 71 int main() { 72 // Example that demonstrates lexicographically smallest trail 73 // Graph: 0-1 (x2 parallel), 0-2, 1-2, 1-3 74 LexiEuler G(4); 75 G.add_edge(0,1); 76 G.add_edge(0,1); // parallel edge 77 G.add_edge(0,2); 78 G.add_edge(1,2); 79 G.add_edge(1,3); 80 81 auto trail = G.smallest_euler(); 82 if (trail.empty()) { 83 std::cout << "No Euler trail\n"; 84 } else { 85 std::cout << "Lexicographically smallest Euler trail: "; 86 for (size_t i = 0; i < trail.size(); ++i) { 87 if (i) std::cout << ' '; 88 std::cout << trail[i]; 89 } 90 std::cout << '\n'; 91 } 92 return 0; 93 } 94
This implementation always takes the smallest available neighbor using a multiset. It produces the lexicographically smallest Euler trail among all valid trails starting at the chosen start vertex (smallest odd vertex if a path, otherwise the smallest vertex with nonzero degree). The trade-off is a logarithmic factor per edge operation due to multiset maintenance.