DSA DSA In Depth
0%
lesson eulerian-pathhierholzerde-bruijn

Eulerian Trails & Circuits: Degree Conditions & Hierholzer's O(E) Algorithm

7 min read Module 5.1: Graph Representations, Traversals, Connectivity & Topological Order Updated Oct 23, 2026
📄 Cheat Sheet
Eulerian Path and Circuit Algorithm (Hierholzer's Algorithm)
William Fiset Eulerian Path and Circuit Algorithm (Hierholzer's Algorithm) Watch on YouTube ↗

Demonstrates degree parity conditions and how Hierholzer's post-order splicing avoids O(E^2) dead-end backtracking.

0:00 / --:--

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).

Formal Theorem — Euler-Hierholzer Existence Conditions
  • 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):

  1. 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$.
  2. Only when $u$ has zero remaining outgoing edges do we append $u$ to the output vector trail.
  3. Finally, reverse trail. If trail.size() == E + 1, the graph is connected on its edge set and trail is the exact Eulerian path; otherwise the edges belong to disconnected components!
Hierholzer Post-Order Splicing: Main Walk + Detour Cycle at Vertex 1 0 1 2 3 4 1. Walk 0 → 1 → 2 (2 finishes) 2. Back at 1: detour 1→3→4→1 3. Post-order: [2, 1, 4, 3, 1, 0] 4. Reversed: 0→1→3→4→1→2
flowchart LR Deg["Verify Degree Parity Conditions"] --> Start["Pick start vertex s; push s to stack"] Start --> Top{"u = st.back() has unused edge?"} Top -- "Yes" --> Advance["Consume edge (u, v); push v to stack"] Advance --> Top Top -- "No" --> Emit["Pop u and append u to trail"] Emit --> Empty{"Stack empty?"} Empty -- "No" --> Top Empty -- "Yes" --> Rev["Reverse trail & verify trail.size() == E + 1"]
CSR (offsets/edges) Memory Layout & Hierholzer Eulerian Circuit Visualizer
O(V + E) Contiguous CSR · O(E) Hierholzer

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.

Step 1 / 11
Custom Input:
Timeline

Invariant & State

CSR Slice Invariant: N_out(u0) = edges[offsets[0] .. offsets[1]) = edges[0..3) (3 contiguous neighbors).
e[0] e[1] e[2] e[3] e[4] e[5] e[6] e[7] u0 [0..3) u1 [3..5) u2 [5..6) u3 [6..8) u4 [8..8)
offsets[0..5] (V+1 = 6 entries; slice for u0 is [offsets[0]=0, offsets[1]=3)):
[0]0start=0
[1]3end=3
[2]5
[3]6
[4]8
[5]8
edges[0..7] (Contiguous E = 8 target IDs in single cache-line allocation):
[0]u1u0→u1
[1]u2u0→u2
[2]u3u0→u3
[3]u2
[4]u4
[5]u4
[6]u1
[7]u4
Inspected Vertex
u0
CSR Slice [L, R)
[0, 3)
Out-Degree
offsets[1] - offsets[0] = 3
Cache Locality
Stride-1 (0 heap ptrs)
[Step 1] Inspecting u0: offsets[0]=0, offsets[1]=3 -> neighbors [u1, u2, u3].
Hardware Reality — Current-Arc Cursor vs. std::set Edge Deletion

Deleting 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;
}
Failure Mode — Passing Degree Checks on Disconnected Edge Components

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.

Check yourself Question 1 of 4
Concept Check Directed Eulerian Trail Degree Condition recall
Section: Degree Parity Conditions

Assume all edges of a directed graph $G$ belong to a single weakly connected component. Under what exact degree condition does $G$ possess a directed Eulerian trail (path) that is NOT a closed circuit?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

An input graph has $V = 6$ vertices and $E = 6$ edges forming two disconnected triangles ($0-1-2-0$ and $3-4-5-3$). Every vertex has degree $2$ (even). How does a production Hierholzer implementation detect that no Eulerian circuit exists?

Key Takeaways

An undirected graph has an Eulerian circuit iff every vertex has even degree (and all edges lie in one connected component), and an Eulerian trail iff 0 or 2 vertices have odd degree.
A directed graph has an Eulerian trail iff at most one vertex has out - in = 1, at most one has in - out = 1, and all others have in == out.
Hierholzer's algorithm runs in O(V + E) time by advancing a per-vertex adjacency cursor and emitting vertices in post-order when their outgoing edges are exhausted.
Always verify that the resulting vertex sequence has length exactly E + 1 to guard against disconnected cycles passing the local degree test.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue