Necessary & Sufficient Degree Conditions
Assume all vertices with non-zero degree belong to a single connected component (or in the directed case, a single weakly connected component whose underlying active vertices satisfy the degree conditions below).
- Undirected Eulerian Circuit (Closed Tour): Exists iff every vertex has even degree ($\forall u, \text{deg}(u) \equiv 0 \pmod 2$).
- Undirected Eulerian Trail (Open Path): Exists iff $0$ or $2$ vertices have odd degree. If $2$ vertices have odd degree, one must be the start vertex and the other the end vertex.
- Directed Eulerian Circuit: Exists iff every vertex belongs to the same strongly connected component and satisfies $\text{in-deg}(u) == \text{out-deg}(u)$.
- Directed Eulerian Trail: Exists iff at most one vertex has $\text{out-deg}(s) - \text{in-deg}(s) = 1$ (start $s$), at most one vertex has $\text{in-deg}(t) - \text{out-deg}(t) = 1$ (end $t$), and all other vertices have $\text{in-deg}(u) == \text{out-deg}(u)$.
How Hierholzer's Algorithm Splices Sub-Cycles in $O(E)$
Suppose we start at $s$ and greedily follow unused outgoing edges, deleting each edge as we traverse it. By the degree invariant, whenever we enter any vertex $v \neq t$, there is at least one unused outgoing edge leaving $v$. Thus the greedy walk can only get stuck at the destination vertex $t$ (or $s$ for a circuit)!
However, this initial walk $W$ might bypass some edges if the graph contains side-cycles. Instead of splicing linked lists mid-walk, Hierholzer's post-order formulation maintains a per-vertex edge cursor head[u] (just like Dinic's current-arc pointer):
- While vertex $u$ still has unused outgoing edges, grab the next edge $e = (u, v)$, advance
head[u]++, and recurse (or push to an explicit stack) on $v$. - Only when $u$ has zero remaining outgoing edges do we append $u$ to the output vector
trail. - Finally, reverse
trail. Iftrail.size() == E + 1, the graph is connected on its edge set andtrailis the exact Eulerian path; otherwise the edges belong to disconnected components!
Inspect how Compressed Sparse Row (CSR) packs all adjacency lists into two flat arrays offsets[V+1] and edges[E] so vertex u’s neighbors live in slice edges[offsets[u]..offsets[u+1]), and trace Hierholzer’s O(E) stack splice for Eulerian circuits.
Invariant & State
std::set Edge DeletionDeleting traversed edges from a std::set<int> or std::vector::erase degrades Hierholzer's algorithm to $O(E \log V)$ or $O(E^2)$. Instead, maintain an index cursor idx[u] into each vertex's adjacency vector (advancing idx[u]++) and for undirected graphs mark a flat boolean array usedEdge[edgeId] = 1. This runs in strict $O(V + E)$ with sequential array scans.
Complexity Matrix
| Algorithm | Time Complexity | Space Complexity | Bottleneck |
|---|---|---|---|
| Hierholzer's Algorithm (Cursor + Stack) | O(V + E) | O(V + E) | Single linear scan over adjacency lists |
| Fleury's Algorithm (Bridge Check per Step) | O(E²) | O(V + E) | Re-running bridge detection on every edge |
| De Bruijn Sequence Generation ($k^n$) | O(k^n) | O(k^n) | Eulerian circuit on $k^{n-1}$-state De Bruijn graph |
Production Implementation: Iterative Hierholzer's Algorithm (Directed & Undirected)
#include <vector>
#include <algorithm>
// Finds a Directed Eulerian Trail/Circuit in O(V + E). Returns empty vector if none exists.
std::vector<int> findDirectedEulerianTrail(int n, int m, const std::vector<std::vector<int>>& adj) {
std::vector<int> in(n, 0), out(n, 0);
for (int u = 0; u < n; ++u) {
out[u] = static_cast<int>(adj[u].size());
for (int v : adj[u]) ++in[v];
}
int start = -1, plusOne = 0, minusOne = 0;
for (int u = 0; u < n; ++u) {
int diff = out[u] - in[u];
if (diff == 1) { ++plusOne; start = u; }
else if (diff == -1) { ++minusOne; }
else if (diff != 0) return {};
}
if (!((plusOne == 0 && minusOne == 0) || (plusOne == 1 && minusOne == 1))) return {};
if (start == -1) {
start = 0;
for (int u = 0; u < n; ++u) {
if (out[u] > 0) { start = u; break; }
}
}
std::vector<size_t> cursor(n, 0);
std::vector<int> st = {start}, trail;
trail.reserve(m + 1);
while (!st.empty()) {
int u = st.back();
if (cursor[u] < adj[u].size()) {
int v = adj[u][cursor[u]++];
st.push_back(v);
} else {
trail.push_back(u);
st.pop_back();
}
}
if (static_cast<int>(trail.size()) != m + 1) return {}; // Disconnected edge components
std::reverse(trail.begin(), trail.end());
return trail;
}
Consider a graph consisting of two disjoint triangles ($C_3 \cup C_3$). Every vertex has degree $2$ (even!), so a naive check that only verifies vertex degrees declares the graph Eulerian and returns a circuit of length $3$ instead of $6$. Always check trail.size() == E + 1 after Hierholzer's loop finishes to verify that all $E$ edges belong to the same connected component.