Skip to content

Eulerian Paths and Reconstruct Itinerary

Every graph problem so far has been about visiting nodes. This one is about visiting edges — walk every edge exactly once, and you have an Eulerian path. It is the problem Euler solved in 1736 with the bridges of Königsberg, which makes it the oldest result in graph theory, and it still appears in interviews as LeetCode 332.

It matters here for one reason: a normal DFS gets it wrong. The natural recursive traversal produces a plausible answer that is not a valid itinerary, and understanding why is the entire lesson.

  • The degree conditions that decide whether an Eulerian path or circuit exists at all — usually answerable in one pass, before writing any traversal.
  • Hierholzer’s algorithm, and why it appends nodes after the recursion rather than before.
  • Why an ordinary DFS fails on LeetCode 332, and what “getting stuck” means.
  • How the lexicographic requirement is satisfied for free by a heap.

The underlying traversal is still a DFS — what changes is when a node is recorded and that edges, not nodes, are consumed:

graphA DFS over edges rather than nodesthe traversal Hierholzer builds on
JFKATLSFO
stack
JFK
seedPush JFK. The *only* structural difference from BFS is that this container pops from the end instead of the front — swap the deque for a list and breadth becomes depth.
1/5

Watch the stack. Hierholzer's insight is that a node should be appended to the answer only when it has no unused edges left — that is, on the way OUT of the recursion, not on the way in. Appending on entry gives the order shown here, which is not a valid itinerary.

Take tickets = [[JFK,SFO], [JFK,ATL], [SFO,ATL], [ATL,JFK], [ATL,SFO]] and ask for the lexicographically smallest itinerary from JFK.

A greedy DFS takes the alphabetically smallest option at every step:

stepattakesremaining edges from here
1JFKATL (before SFO)SFO
2ATLJFKSFO
3JFKSFO
4SFOATL
5ATLSFO
6SFOstuck

It ends at SFO having used all five edges, which happens to work here. Now reorder slightly and the greedy walks into a dead end with edges still unused — and a plain DFS has no way to recover, because it has already committed to the order it printed.

hierholzer.py
from collections import defaultdict
import heapq
 
 
def find_itinerary(tickets):
    # Min-heap per node gives lexicographic order for free.
    graph = defaultdict(list)
    for src, dst in tickets:
        heapq.heappush(graph[src], dst)
 
    route = []
 
    def visit(node):
        # Consume edges until this node is exhausted...
        while graph[node]:
            nxt = heapq.heappop(graph[node])   # removing IS marking used
            visit(nxt)
        # ...then record it. Post-order is the entire trick.
        route.append(node)
 
    visit("JFK")
    return route[::-1]                          # reverse: stuck node ends last
 
 
def find_itinerary_iterative(tickets):
    graph = defaultdict(list)
    for src, dst in tickets:
        heapq.heappush(graph[src], dst)
 
    stack, route = ["JFK"], []
    while stack:
        while graph[stack[-1]]:
            stack.append(heapq.heappop(graph[stack[-1]]))
        route.append(stack.pop())               # exhausted — record it
    return route[::-1]
 
 
tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]]
print(find_itinerary(tickets))
# expect ['JFK', 'MUC', 'LHR', 'SFO', 'SJC']
print(find_itinerary_iterative(tickets))

Note there is no visited set. Edges are marked used by being popped out of the adjacency structure, which is the right model — this is about consuming edges, not marking nodes, and nodes are revisited on purpose.

tickets = [[JFK,SFO], [JFK,ATL], [SFO,ATL], [ATL,JFK], [ATL,SFO]], starting at JFK. Heaps: JFK: [ATL, SFO], ATL: [JFK, SFO], SFO: [ATL].

depthatpopsroute (append order)
1JFKATL
2ATLJFK
3JFKSFO
4SFOATL
5ATLSFO
6SFOexhausted[SFO]
5ATLexhausted[SFO, ATL]
4SFOexhausted[SFO, ATL, SFO]
3JFKexhausted[SFO, ATL, SFO, JFK]
2ATLexhausted[…, ATL]
1JFKexhausted[SFO, ATL, SFO, JFK, ATL, JFK]

Reversed: JFKATLJFKSFOATLSFO — all five edges, each once, lexicographically smallest.

Read the append column again. SFO was recorded first because it got stuck first, and reversing puts it last where it belongs. Every node appears in the answer as many times as it is passed through, which is why nodes are not marked visited.

AspectCost
Feasibility check (degrees + connectivity)O(V+E)O(V + E)
Hierholzer with plain listsO(V+E)O(V + E)
Hierholzer with a heap per node (lexicographic)O(ElogE)O(E \log E)
SpaceO(V+E)O(V + E) for the graph, O(E)O(E) for the route

The log\log factor is the price of the lexicographic requirement, not of the algorithm. Pre-sorting each adjacency list and popping from the end is the same cost overall and slightly faster in practice — worth mentioning.

VariantConditionCanonical problem
Eulerian path, directedconnected; in == out everywhere, or one node out = in + 1 and one in = out + 1332 Reconstruct Itinerary
Eulerian circuit, directedconnected; in == out for every node2097 Valid Arrangement of Pairs
Eulerian path, undirectedconnected; 0 or 2 nodes of odd degreeclassic Königsberg
De Bruijn sequenceEulerian circuit on a de Bruijn graph753 Cracking the Safe
Domino / word chainsmodel each tile as an edge, not a nodeinterview favourite
  • Appending the node before the recursion. Pre-order gives an invalid itinerary. It must be post-order, then reversed — this is the single defining detail.
  • Using a visited set on nodes. Nodes are revisited deliberately. Only edges are consumed, by removing them.
  • Forgetting to reverse. The route is built backwards. The un-reversed output is a valid path through the reverse graph, which makes it look almost right.
  • Confusing Eulerian with Hamiltonian. Every edge once is polynomial; every node once is NP-hard. Reaching for backtracking here is a large, avoidable cost.
  • Skipping the feasibility check. A graph with two nodes of out = in + 1 has no Eulerian path at all, and the traversal will happily return a partial answer rather than saying so.
  • Assuming connectivity. Isolated edges make the path impossible even when every degree is balanced.
They askWhat they’re checkingThe answer
“How do you know an Eulerian path exists?”Whether you check before codingDirected: connected, and either all in == out or exactly one node with out = in + 1 and one with in = out + 1. Undirected: connected, and 0 or 2 odd-degree nodes
“Why post-order and not pre-order?”The defining insightGetting stuck is guaranteed and happens at the end of the itinerary. Recording on exit and reversing puts the stuck node last, where it belongs
“Why no visited set?”Whether you understand the modelEdges are consumed, nodes are not. A valid itinerary revisits airports repeatedly
“How is lexicographic order guaranteed?”Data-structure choiceA min-heap per node, or pre-sort each adjacency list. It costs the log\log factor and nothing else
“Eulerian or Hamiltonian?”Whether you know the boundaryEulerian visits every edge and is O(V+E)O(V + E). Hamiltonian visits every node and is NP-hard — completely different tools
“Recursion depth on 10,000 tickets”Practical limitsDepth equals edge count, so Python’s default limit is exceeded. Use the iterative stack version — which is why it is given above
1 problems
0 easy0 medium1 hard

Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.

Feasibility check — directed Eulerian path

Section titled “Feasibility check — directed Eulerian path”

Iterative Hierholzer — avoiding the recursion limit

Section titled “Iterative Hierholzer — avoiding the recursion limit”
pch.quizTag Eulerian paths — self-check
  1. What distinguishes an Eulerian path from a Hamiltonian path, and why does it matter enormously?

    pch.quizShowAnswer

    B — Eulerian visits every EDGE once and is O(V+E); Hamiltonian visits every NODE once and is NP-hard — Conflating them is expensive: you either reach for backtracking on a polynomial problem, or hunt for a polynomial algorithm that does not exist. 'Nodes may be revisited' is the tell for Eulerian.

  2. Why is the node appended AFTER the recursion rather than before?

    pch.quizShowAnswer

    B — Because getting stuck is guaranteed and happens at the END of the itinerary — recording on exit and reversing puts the stuck node last, where it belongs — This is the whole algorithm. A plain pre-order DFS commits to an order it cannot revise, so it produces a plausible but invalid itinerary when it dead-ends with edges unused.

  3. Why is there no visited set?

    pch.quizShowAnswer

    B — Because edges are consumed by being removed from the adjacency structure — nodes are revisited on purpose, since a valid itinerary passes through airports repeatedly — The model is edge consumption, not node marking. Adding a visited set on nodes would forbid exactly the revisits the answer requires.

  4. For a directed graph, when does an Eulerian PATH (not circuit) exist?

    pch.quizShowAnswer

    B — Connected, and either every node has in == out, or exactly one node has out = in + 1 and exactly one has in = out + 1 — The two unbalanced nodes are the start and the end. Checking this in one O(V+E) pass before writing any traversal is often most of the credit on this problem.

  5. How is the lexicographically smallest itinerary guaranteed?

    pch.quizShowAnswer

    B — By a min-heap per node (or pre-sorting each adjacency list), so the smallest unused destination is always taken first — Sorting the final route would destroy it — the route is a path, not a set. The ordering must be enforced at each choice point, and that costs only the log factor.

  • Cue — use every edge exactly once; nodes may repeat. Answer is an ordering of edges, often lexicographically smallest.
  • Feasibility first — directed: connected, and all in == out, or one node out = in + 1 and one in = out + 1. Undirected: connected, and 0 or 2 odd-degree nodes.
  • Hierholzer — consume edges from a node until exhausted, then append the node; reverse at the end.
  • No visited set — popping the edge is what marks it used.
  • ComplexityO(V+E)O(V + E), or O(ElogE)O(E \log E) with a heap for lexicographic order.
  • Not Hamiltonian — every node once is NP-hard. Check which you have.
  • Eulerian paths are about edges, not nodes, and that single distinction separates a polynomial problem from an NP-hard one.
  • The degree conditions answer “is it even possible” in one pass, before any traversal.
  • Hierholzer’s algorithm works with getting stuck rather than against it: append on the way out, then reverse.
  • LeetCode 332 is the canonical form, and 753 Cracking the Safe is the same algorithm behind a modelling problem.

Next: Union-Find Problem Patterns — the other way to reason about connectivity, and when it beats a traversal.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading