Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Graph Algorithms

Builds on Flood Fill, which you haven’t finished yet.

P3HardMental model~4 min speedrun

Eulerian Path (Hierholzer's Algorithm)

The problem

Reconstruct Itinerary (LC 332)(opens leetcode.com)

Return the airports of one trip, in order, that begins at "JFK" and uses every ticket in tickets exactly once. A ticket [fromi, toi] is one flight from airport fromi to airport toi, and every airport is written as 3 uppercase letters.

More than one trip can use all the tickets. Return the one whose list of airports comes first when the lists are compared airport by airport ("ATL" before "SFO"), which is the same as comparing each trip written as one string. The tickets always allow at least one such trip.

Example 1
Input:tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]]
Output:["JFK","MUC","LHR","SFO","SJC"]
Only one order uses every ticket: JFK -> MUC -> LHR -> SFO -> SJC.
MUCLHRJFKSFOSJC

Eulerian Path

edges ➔ walk and delete each used edge ➔ nodes appended when stuck, then reversed
Spot it when: Every edge (a ticket, a domino, a pair, a transition) must be used exactly once in one walk, or you need the shortest string that contains every combination as a substring.
Eulerian Path
Python
1from collections import defaultdict
2from typing import Hashable, List
3 
4def eulerian_path(edges: List[List[Hashable]]) -> List[Hashable]:
5 """Eulerian Path (Hierholzer's algorithm, the core of Reconstruct Itinerary): the
6 smallest walk that uses every directed edge exactly once.
7 Time: O(E log E) | Space: O(E)
8 """
9 graph = defaultdict(list)
10 balance = defaultdict(int) # out-degree minus in-degree
11 # reverse order, so pop() takes the smallest neighbour
12 for u, v in sorted(edges, reverse=True):
13 graph[u].append(v)
14 balance[u] += 1
15 balance[v] -= 1
16 # one extra edge out starts the path; a circuit starts at the smallest node
17 start = next((x for x in graph if balance[x] == 1), min(graph))
18 path, stack = [], [start]
19 while stack:
20 node = stack[-1]
21 if graph[node]:
22 stack.append(graph[node].pop()) # follow an unused edge and delete it
23 else:
24 # no edge left: node is final in the rest of the walk
25 path.append(stack.pop())
26 return path[::-1] # nodes were finished last-first
Invariant: Every edge is followed at most once and deleted when it is, stack is a walk from start over used edges, and path holds, last-first, the part of the Eulerian path that is already final.

Walk from start, deleting each edge as you follow it with stack.append(graph[node].pop()), and add a node to path only when it has no unused edge left, with path.append(stack.pop()); reversing path gives the walk. This is correct because a walk that always takes some unused edge can only get stuck at the node where the whole path must end, so the first node to run out of edges is the last node of the answer; any cycle the walk skipped hangs off a node that is still on stack, and it is walked, and placed, when the stack backs up to that node. The trap is writing a node into the answer when you first reach it: the walk can enter the final dead end before a cycle has been used (in Reconstruct Itinerary (LC 332), taking the smallest airport first can strand the tickets of a loop), so a node goes into path only once its edges are used up, and the list is reversed at the end.

See it run

Hierholzer's walk, step by step

unused 10 · path []

unused 9 · path []

unused 8 · path []

unused 7 · path []

unused 6 · path []

unused 6 · path [KUL]

unused 5 · path [KUL]

unused 4 · path [KUL]

unused 3 · path [KUL]

unused 3 · path [KUL, JFK]

unused 3 · path [KUL, JFK, LAX]

unused 2 · path [KUL, JFK, LAX]

unused 1 · path [KUL, JFK, LAX]

unused 0 · path [KUL, JFK, LAX]

unused 0 · path [KUL, JFK, LAX, NRT]

unused 0 · path [KUL, JFK, LAX, NRT, ICN]

unused 0 · path [KUL, JFK, LAX, NRT, ICN, SFO]

unused 0 · path [KUL, JFK, LAX, NRT, ICN, SFO, NRT]

unused 0 · path [KUL, JFK, LAX, NRT, ICN, SFO, NRT, JFK]

unused 0 · path [KUL, JFK, LAX, NRT, ICN, SFO, NRT, JFK, ORD]

unused 0 · path [KUL, JFK, LAX, NRT, ICN, SFO, NRT, JFK, ORD, ATL]

unused 0 · path [KUL, JFK, LAX, NRT, ICN, SFO, NRT, JFK, ORD, ATL, JFK]

unused 0 · path [KUL, JFK, LAX, NRT, ICN, SFO, NRT, JFK, ORD, ATL, JFK] · answer [JFK, ATL, ORD, JFK, NRT, SFO, ICN, NRT, LAX, JFK, KUL]

Each circle is an airport and each arrow is an unused ticket. A ticket disappears from the picture after the step that uses it. The rows under the graph are stack and path.

  • now the airport on top of stack, or the ticket just used
  • stack an airport on the current walk
  • final an airport already moved to path

JFK has one more ticket going out than coming in, so the walk must start there. Put JFK on stack.

The top of stack is JFK. Its smallest unused ticket goes to ATL. Delete that ticket and push ATL.

The top of stack is ATL. Its smallest unused ticket goes to ORD. Delete that ticket and push ORD.

The top of stack is ORD. Its smallest unused ticket goes to JFK. Delete that ticket and push JFK.

The top of stack is JFK. Its smallest unused ticket goes to KUL. Delete that ticket and push KUL.

KUL has no unused ticket left. Move it from stack to path. This is the first dead end, so KUL is the last stop of the itinerary.

The top of stack is JFK. Its smallest unused ticket goes to NRT. Delete that ticket and push NRT.

The top of stack is NRT. Its smallest unused ticket goes to LAX. Delete that ticket and push LAX.

The top of stack is LAX. Its smallest unused ticket goes to JFK. Delete that ticket and push JFK.

JFK has no unused ticket left. Move it from stack to path. Now LAX is on top and has no unused tickets either.

LAX has no unused ticket left. Move it from stack to path. Now NRT is on top and still has unused tickets.

The top of stack is NRT. Its smallest unused ticket goes to SFO. Delete that ticket and push SFO.

The top of stack is SFO. Its smallest unused ticket goes to ICN. Delete that ticket and push ICN.

The top of stack is ICN. Its smallest unused ticket goes to NRT. Delete that ticket and push NRT.

NRT has no unused ticket left. Move it from stack to path. Now ICN is on top and has no unused tickets either.

ICN has no unused ticket left. Move it from stack to path. Now SFO is on top and has no unused tickets either.

SFO has no unused ticket left. Move it from stack to path. Now NRT is on top and has no unused tickets either.

NRT has no unused ticket left. Move it from stack to path. Now JFK is on top and has no unused tickets either.

JFK has no unused ticket left. Move it from stack to path. Now ORD is on top and has no unused tickets either.

ORD has no unused ticket left. Move it from stack to path. Now ATL is on top and has no unused tickets either.

ATL has no unused ticket left. Move it from stack to path. Now JFK is on top and has no unused tickets either.

JFK has no unused ticket left. Move it from stack to path.

Every ticket is used, and stack is empty. path holds the stops last-first, so reverse it. The itinerary is [JFK, ATL, ORD, JFK, NRT, SFO, ICN, NRT, LAX, JFK, KUL].

JFKnowATLORDKULNRTSFOICNLAX
stack
JFK
path
empty
1 / 23
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Writing each node into the answer when first reached

    Mistake
    path.append(node)
    while graph[node]:
        node = graph[node].pop()
        path.append(node)
    Fix
    if graph[node]:
        stack.append(graph[node].pop())
    else:
        path.append(stack.pop())

    What happens: The walk can enter the final dead end before a cycle has been used. Written in the order reached, the answer stops at that dead end. The cycle's edges are never used. The blueprint adds a node to path only when its edges are used up, then reverses path.

    Example: Reconstruct Itinerary (LC 332): tickets = [["JFK", "KUL"], ["JFK", "NRT"], ["NRT", "JFK"]]
    Mistake: returns ['JFK', 'KUL']Fix: returns ['JFK', 'NRT', 'JFK', 'KUL']
  2. 2

    Gotcha 2: Starting at the smallest node instead of the unbalanced one

    Mistake
    start = min(graph)
    Fix
    start = next((x for x in graph if balance[x] == 1), min(graph))

    What happens: A path that is not a circuit must start at the node with one more edge out than in, balance[x] == 1. That node has one extra edge out, so the walk must leave it first. Started anywhere else, the walk gets stuck before it uses every edge. Only when every node is balanced is the path a circuit. Then it starts at the smallest node, min(graph).

    Example: edges = [[1, 0], [0, 2]]
    Mistake: returns [0, 2]Fix: returns [1, 0, 2]
  3. 3

    Gotcha 3: Sorting the edges ascending, so pop() takes the largest

    Mistake
    for u, v in sorted(edges):
    Fix
    for u, v in sorted(edges, reverse=True):

    What happens: pop() takes the last item of a list in O(1). Sorted in reverse, the last item is the smallest neighbour. Sorted ascending, pop() takes the largest neighbour instead. The walk still uses every edge, but it is not the smallest walk. pop(0) would take the smallest, but it costs O(E) per step.

    Example: tickets = [["JFK", "SFO"], ["JFK", "ATL"], ["SFO", "ATL"], ["ATL", "JFK"], ["ATL", "SFO"]]
    Mistake: returns ['JFK', 'SFO', 'ATL', 'JFK', 'ATL', 'SFO']Fix: returns ['JFK', 'ATL', 'JFK', 'SFO', 'ATL', 'SFO']
  4. 4

    Gotcha 4: Returning path without reversing it

    Mistake
    return path
    Fix
    return path[::-1]

    What happens: A node goes into path when it has no edge left. The final node of the walk runs out first. So path holds the walk from its end back to its start. path[::-1] puts it in order.

    Example: tickets = [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]]
    Mistake: returns ['SJC', 'SFO', 'LHR', 'MUC', 'JFK']Fix: returns ['JFK', 'MUC', 'LHR', 'SFO', 'SJC']
Not this one if…
  • Every node must appear once, after everything it depends on, as in Course Schedule II (LC 210): that is an order of the nodes, not a walk over every edge. See Topological Sort
  • You only need to know which nodes the edges join, as in Number of Provinces (LC 547), not an order in which to use each edge. See Flood Fill

Deep Dive

Why a node is final only when its tickets run out

An Eulerian path is a walk that uses every edge exactly once. In Reconstruct Itinerary (LC 332), each ticket is an edge, and the itinerary must use every ticket once. Hierholzer's algorithm builds the walk with one stack and one path.

The loop looks at the top of stack. If that airport has an unused ticket, the loop deletes the ticket with graph[node].pop() and pushes the airport it leads to. If the airport has no unused ticket, the loop moves it from stack to path.

The first airport that runs out of tickets is the last stop of the itinerary. The reason is a count. Every airport except the start and the end has as many tickets out as in. Each time the walk enters such an airport, it uses one ticket in, so one ticket out is still left. The walk can only get stuck at the end airport.

The walk may leave some tickets unused. Those tickets form loops that start and end at an airport already on stack. When the loop backs up to such an airport, it walks the loop, and the loop is placed in the right spot, because its airports reach path before the airports under them.

Example: for tickets = [["JFK", "KUL"], ["JFK", "NRT"], ["NRT", "JFK"]], the smallest-first walk goes JFK -> KUL and gets stuck, with the loop JFK -> NRT -> JFK still unused. The correct itinerary is ["JFK", "NRT", "JFK", "KUL"].

One trace: ten tickets, one dead end, two loops

The board uses 10 tickets and 8 airports. The airport JFK is the start, because it has one more ticket out than in. The airport KUL is a dead end with no ticket out. The tickets are JFK -> ATL, ATL -> ORD, ORD -> JFK, JFK -> KUL, JFK -> NRT, NRT -> SFO, SFO -> ICN, ICN -> NRT, NRT -> LAX and LAX -> JFK.

The walk goes JFK, ATL, ORD, JFK. At JFK the smallest unused ticket goes to KUL, so the walk goes there and gets stuck. KUL moves to path. This is the first dead end, and KUL is the last stop.

The loop backs up to JFK, which still has a ticket to NRT. The walk goes NRT, LAX, JFK and gets stuck again, because JFK has no ticket left. JFK and LAX move to path. At NRT, the ticket to SFO is still unused. The walk goes SFO, ICN, NRT, and then everything moves to path in turn.

The final path is [KUL, JFK, LAX, NRT, ICN, SFO, NRT, JFK, ORD, ATL, JFK]. Reversed, it is [JFK, ATL, ORD, JFK, NRT, SFO, ICN, NRT, LAX, JFK, KUL]. The board uses 10 steps that delete a ticket and 11 steps that move an airport to path.

The traps: writing on arrival, the start, the sort and the reversal

Do not write an airport into the answer when you first reach it. On the board tickets, the greedy walk goes JFK, ATL, ORD, JFK, KUL and stops, because it is stuck at KUL. It used 4 of the 10 tickets. The blueprint writes an airport only when its tickets are used up, so the loops are not lost.

The start must be the airport with balance[x] == 1. For edges = [[1, 0], [0, 2]], the correct walk is [1, 0, 2]. Starting at the smallest node, 0, gives [0, 2] and leaves the ticket 1 -> 0 unused. Only when every airport is balanced is the path a circuit, and then it starts at min(graph).

The tickets are sorted with reverse=True, so pop() takes the smallest neighbour in O(1). With an ascending sort, pop() takes the largest. On the board tickets, the ascending sort gives [JFK, NRT, SFO, ICN, NRT, LAX, JFK, ATL, ORD, JFK, KUL]. This is a valid walk, but the correct answer goes JFK, ATL and not JFK, NRT.

The last line is path[::-1]. Airports reach path last-first. Without the reversal, the example [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]] returns [SJC, SFO, LHR, MUC, JFK] and not [JFK, MUC, LHR, SFO, SJC].

The cost, and the neighbours

Sorting the tickets costs O(E log E). The walk then deletes each ticket once and moves each airport to path once, which is O(E). The total is O(E log E) time. graph, balance, stack and path hold O(E) entries, so the space is O(E).

On the board, 10 tickets give 11 airports in the answer. Each ticket is deleted in one step, and each of the 11 stops reaches path in one step.

The same walk solves Valid Arrangement of Pairs (LC 2097), where the pairs are the edges and any valid order is accepted. Cracking the Safe (LC 753) uses it on a graph whose nodes are strings, and the walk gives the shortest string that contains every combination.

The Topological Sort card solves a different question. In Course Schedule II (LC 210), every node appears once, after everything it depends on. That is an order of the nodes. Here every edge is used once, and a node can appear many times. If you only need to know which nodes are joined, a plain search is enough, as in Number of Provinces (LC 547).

60-Second Pitch

"Every ticket must be used once, so this is an Eulerian path, and I use Hierholzer's algorithm. I sort the tickets in reverse and keep them in a list for each airport, so pop() takes the smallest destination. I start at the airport with one more ticket out than in, or at the smallest airport if all are balanced. I keep a stack. If the top airport has an unused ticket, I delete it and push the destination. If it has none, I move the airport to path. An airport goes into path only when its tickets are used up. So a dead end is placed last, and a loop that the walk skipped is walked when the stack backs up to its airport. At the end I return path reversed. Sorting costs O(E log E), and the walk costs O(E). The space is O(E)."

Common Questions

Why does the walk get stuck only at the end airport?

Every airport except the start and the end has as many tickets out as in. Each entry into such an airport uses one ticket in, so one ticket out is still unused. The start has one extra ticket out. So the only airport where the walk can run out of tickets is the end.

Why do you add an airport to path when it is stuck, and not when it is reached?

The first stuck airport is the last stop. A loop that the walk skipped starts and ends at an airport that is still on stack. If airports were written on arrival, the dead end would be written before the loop, and the loop would be lost. With the stuck rule, the loop is walked and placed before the airports under it.

How do you get the smallest itinerary?

Sort the tickets in reverse and use pop(), so each airport always takes its smallest unused destination first. Hierholzer's algorithm with this rule returns the smallest valid walk for Reconstruct Itinerary (LC 332).

What are the time and space complexity?

O(E log E) time, because of the sort, and O(E) space. The walk itself deletes each ticket once and moves each airport to path once.

How do you know that a valid walk exists?

Check the counts. In a directed graph, an Eulerian path needs every airport balanced, except at most one with one more ticket out than in (the start) and one with one more ticket in than out (the end). The edges must also be joined in one piece. Reconstruct Itinerary (LC 332) promises that a valid itinerary exists.

SWE Reasoning

Spot it

  • Every ticket, domino, pair or transition must be used exactly once in one walk.
  • The question asks for the walk or the order of the stops, and not only whether it exists.
  • The wording says itinerary, arrangement, or the shortest string that contains every combination.
  • When two choices are possible, the smallest one must win, so the neighbours are sorted.
  • Trying every order of the edges would take O(E!) time, which is far too slow.

The invariant, step by step

  1. The tickets are sorted in reverse order, so graph[u].pop() takes the smallest neighbour of u.
  2. start is the airport with balance[x] == 1. If every airport is balanced, start is the smallest airport.
  3. The loop looks at node = stack[-1]. If graph[node] is not empty, it deletes the smallest ticket with pop() and pushes the airport it leads to.
  4. If graph[node] is empty, the loop moves node from stack to path. No edge leaves this airport any more, so it is final in the rest of the walk.
  5. stack is always a walk from start over used tickets, and path holds the final part of the answer, last-first.
  6. When stack is empty, every ticket was used. path[::-1] is the itinerary.

Traps

  • Writing an airport into the answer when it is first reached. The walk can enter the final dead end before a loop is used, and the loop is lost. Add an airport to path only when its tickets run out.
  • Starting at the smallest airport. A walk that is not a circuit must start at the airport with balance[x] == 1.
  • Sorting ascending. Then pop() takes the largest neighbour, and the walk is not the smallest one. Sort with reverse=True.
  • Using pop(0) for the smallest neighbour. It moves every other item, so each step costs O(E). Sort in reverse and use pop().
  • Returning path without reversing it. Airports reach path last-first. Return path[::-1].

Practice

Canonical LeetCode:Reconstruct Itinerary (LC 332)Valid Arrangement of Pairs (LC 2097)Cracking the Safe (LC 753)
Theory Context•Graph Algorithms

Builds on Flood Fill, which you haven’t finished yet.

P3HardMental model~4 min speedrun

Eulerian Path (Hierholzer's Algorithm)

The problem

Reconstruct Itinerary (LC 332)(opens leetcode.com)

Return the airports of one trip, in order, that begins at "JFK" and uses every ticket in tickets exactly once. A ticket [fromi, toi] is one flight from airport fromi to airport toi, and every airport is written as 3 uppercase letters.

More than one trip can use all the tickets. Return the one whose list of airports comes first when the lists are compared airport by airport ("ATL" before "SFO"), which is the same as comparing each trip written as one string. The tickets always allow at least one such trip.

Example 1
Input:tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]]
Output:["JFK","MUC","LHR","SFO","SJC"]
Only one order uses every ticket: JFK -> MUC -> LHR -> SFO -> SJC.
MUCLHRJFKSFOSJC

Eulerian Path

edges ➔ walk and delete each used edge ➔ nodes appended when stuck, then reversed
Spot it when: Every edge (a ticket, a domino, a pair, a transition) must be used exactly once in one walk, or you need the shortest string that contains every combination as a substring.
Eulerian Path
Python
1from collections import defaultdict
2from typing import Hashable, List
3 
4def eulerian_path(edges: List[List[Hashable]]) -> List[Hashable]:
5 """Eulerian Path (Hierholzer's algorithm, the core of Reconstruct Itinerary): the
6 smallest walk that uses every directed edge exactly once.
7 Time: O(E log E) | Space: O(E)
8 """
9 graph = defaultdict(list)
10 balance = defaultdict(int) # out-degree minus in-degree
11 # reverse order, so pop() takes the smallest neighbour
12 for u, v in sorted(edges, reverse=True):
13 graph[u].append(v)
14 balance[u] += 1
15 balance[v] -= 1
16 # one extra edge out starts the path; a circuit starts at the smallest node
17 start = next((x for x in graph if balance[x] == 1), min(graph))
18 path, stack = [], [start]
19 while stack:
20 node = stack[-1]
21 if graph[node]:
22 stack.append(graph[node].pop()) # follow an unused edge and delete it
23 else:
24 # no edge left: node is final in the rest of the walk
25 path.append(stack.pop())
26 return path[::-1] # nodes were finished last-first
Invariant: Every edge is followed at most once and deleted when it is, stack is a walk from start over used edges, and path holds, last-first, the part of the Eulerian path that is already final.

Walk from start, deleting each edge as you follow it with stack.append(graph[node].pop()), and add a node to path only when it has no unused edge left, with path.append(stack.pop()); reversing path gives the walk. This is correct because a walk that always takes some unused edge can only get stuck at the node where the whole path must end, so the first node to run out of edges is the last node of the answer; any cycle the walk skipped hangs off a node that is still on stack, and it is walked, and placed, when the stack backs up to that node. The trap is writing a node into the answer when you first reach it: the walk can enter the final dead end before a cycle has been used (in Reconstruct Itinerary (LC 332), taking the smallest airport first can strand the tickets of a loop), so a node goes into path only once its edges are used up, and the list is reversed at the end.

See it run

Hierholzer's walk, step by step

unused 10 · path []

unused 9 · path []

unused 8 · path []

unused 7 · path []

unused 6 · path []

unused 6 · path [KUL]

unused 5 · path [KUL]

unused 4 · path [KUL]

unused 3 · path [KUL]

unused 3 · path [KUL, JFK]

unused 3 · path [KUL, JFK, LAX]

unused 2 · path [KUL, JFK, LAX]

unused 1 · path [KUL, JFK, LAX]

unused 0 · path [KUL, JFK, LAX]

unused 0 · path [KUL, JFK, LAX, NRT]

unused 0 · path [KUL, JFK, LAX, NRT, ICN]

unused 0 · path [KUL, JFK, LAX, NRT, ICN, SFO]

unused 0 · path [KUL, JFK, LAX, NRT, ICN, SFO, NRT]

unused 0 · path [KUL, JFK, LAX, NRT, ICN, SFO, NRT, JFK]

unused 0 · path [KUL, JFK, LAX, NRT, ICN, SFO, NRT, JFK, ORD]

unused 0 · path [KUL, JFK, LAX, NRT, ICN, SFO, NRT, JFK, ORD, ATL]

unused 0 · path [KUL, JFK, LAX, NRT, ICN, SFO, NRT, JFK, ORD, ATL, JFK]

unused 0 · path [KUL, JFK, LAX, NRT, ICN, SFO, NRT, JFK, ORD, ATL, JFK] · answer [JFK, ATL, ORD, JFK, NRT, SFO, ICN, NRT, LAX, JFK, KUL]

Each circle is an airport and each arrow is an unused ticket. A ticket disappears from the picture after the step that uses it. The rows under the graph are stack and path.

  • now the airport on top of stack, or the ticket just used
  • stack an airport on the current walk
  • final an airport already moved to path

JFK has one more ticket going out than coming in, so the walk must start there. Put JFK on stack.

The top of stack is JFK. Its smallest unused ticket goes to ATL. Delete that ticket and push ATL.

The top of stack is ATL. Its smallest unused ticket goes to ORD. Delete that ticket and push ORD.

The top of stack is ORD. Its smallest unused ticket goes to JFK. Delete that ticket and push JFK.

The top of stack is JFK. Its smallest unused ticket goes to KUL. Delete that ticket and push KUL.

KUL has no unused ticket left. Move it from stack to path. This is the first dead end, so KUL is the last stop of the itinerary.

The top of stack is JFK. Its smallest unused ticket goes to NRT. Delete that ticket and push NRT.

The top of stack is NRT. Its smallest unused ticket goes to LAX. Delete that ticket and push LAX.

The top of stack is LAX. Its smallest unused ticket goes to JFK. Delete that ticket and push JFK.

JFK has no unused ticket left. Move it from stack to path. Now LAX is on top and has no unused tickets either.

LAX has no unused ticket left. Move it from stack to path. Now NRT is on top and still has unused tickets.

The top of stack is NRT. Its smallest unused ticket goes to SFO. Delete that ticket and push SFO.

The top of stack is SFO. Its smallest unused ticket goes to ICN. Delete that ticket and push ICN.

The top of stack is ICN. Its smallest unused ticket goes to NRT. Delete that ticket and push NRT.

NRT has no unused ticket left. Move it from stack to path. Now ICN is on top and has no unused tickets either.

ICN has no unused ticket left. Move it from stack to path. Now SFO is on top and has no unused tickets either.

SFO has no unused ticket left. Move it from stack to path. Now NRT is on top and has no unused tickets either.

NRT has no unused ticket left. Move it from stack to path. Now JFK is on top and has no unused tickets either.

JFK has no unused ticket left. Move it from stack to path. Now ORD is on top and has no unused tickets either.

ORD has no unused ticket left. Move it from stack to path. Now ATL is on top and has no unused tickets either.

ATL has no unused ticket left. Move it from stack to path. Now JFK is on top and has no unused tickets either.

JFK has no unused ticket left. Move it from stack to path.

Every ticket is used, and stack is empty. path holds the stops last-first, so reverse it. The itinerary is [JFK, ATL, ORD, JFK, NRT, SFO, ICN, NRT, LAX, JFK, KUL].

JFKnowATLORDKULNRTSFOICNLAX
stack
JFK
path
empty
1 / 23
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Writing each node into the answer when first reached

    Mistake
    path.append(node)
    while graph[node]:
        node = graph[node].pop()
        path.append(node)
    Fix
    if graph[node]:
        stack.append(graph[node].pop())
    else:
        path.append(stack.pop())

    What happens: The walk can enter the final dead end before a cycle has been used. Written in the order reached, the answer stops at that dead end. The cycle's edges are never used. The blueprint adds a node to path only when its edges are used up, then reverses path.

    Example: Reconstruct Itinerary (LC 332): tickets = [["JFK", "KUL"], ["JFK", "NRT"], ["NRT", "JFK"]]
    Mistake: returns ['JFK', 'KUL']Fix: returns ['JFK', 'NRT', 'JFK', 'KUL']
  2. 2

    Gotcha 2: Starting at the smallest node instead of the unbalanced one

    Mistake
    start = min(graph)
    Fix
    start = next((x for x in graph if balance[x] == 1), min(graph))

    What happens: A path that is not a circuit must start at the node with one more edge out than in, balance[x] == 1. That node has one extra edge out, so the walk must leave it first. Started anywhere else, the walk gets stuck before it uses every edge. Only when every node is balanced is the path a circuit. Then it starts at the smallest node, min(graph).

    Example: edges = [[1, 0], [0, 2]]
    Mistake: returns [0, 2]Fix: returns [1, 0, 2]
  3. 3

    Gotcha 3: Sorting the edges ascending, so pop() takes the largest

    Mistake
    for u, v in sorted(edges):
    Fix
    for u, v in sorted(edges, reverse=True):

    What happens: pop() takes the last item of a list in O(1). Sorted in reverse, the last item is the smallest neighbour. Sorted ascending, pop() takes the largest neighbour instead. The walk still uses every edge, but it is not the smallest walk. pop(0) would take the smallest, but it costs O(E) per step.

    Example: tickets = [["JFK", "SFO"], ["JFK", "ATL"], ["SFO", "ATL"], ["ATL", "JFK"], ["ATL", "SFO"]]
    Mistake: returns ['JFK', 'SFO', 'ATL', 'JFK', 'ATL', 'SFO']Fix: returns ['JFK', 'ATL', 'JFK', 'SFO', 'ATL', 'SFO']
  4. 4

    Gotcha 4: Returning path without reversing it

    Mistake
    return path
    Fix
    return path[::-1]

    What happens: A node goes into path when it has no edge left. The final node of the walk runs out first. So path holds the walk from its end back to its start. path[::-1] puts it in order.

    Example: tickets = [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]]
    Mistake: returns ['SJC', 'SFO', 'LHR', 'MUC', 'JFK']Fix: returns ['JFK', 'MUC', 'LHR', 'SFO', 'SJC']
Not this one if…
  • Every node must appear once, after everything it depends on, as in Course Schedule II (LC 210): that is an order of the nodes, not a walk over every edge. See Topological Sort
  • You only need to know which nodes the edges join, as in Number of Provinces (LC 547), not an order in which to use each edge. See Flood Fill

Deep Dive

Why a node is final only when its tickets run out

An Eulerian path is a walk that uses every edge exactly once. In Reconstruct Itinerary (LC 332), each ticket is an edge, and the itinerary must use every ticket once. Hierholzer's algorithm builds the walk with one stack and one path.

The loop looks at the top of stack. If that airport has an unused ticket, the loop deletes the ticket with graph[node].pop() and pushes the airport it leads to. If the airport has no unused ticket, the loop moves it from stack to path.

The first airport that runs out of tickets is the last stop of the itinerary. The reason is a count. Every airport except the start and the end has as many tickets out as in. Each time the walk enters such an airport, it uses one ticket in, so one ticket out is still left. The walk can only get stuck at the end airport.

The walk may leave some tickets unused. Those tickets form loops that start and end at an airport already on stack. When the loop backs up to such an airport, it walks the loop, and the loop is placed in the right spot, because its airports reach path before the airports under them.

Example: for tickets = [["JFK", "KUL"], ["JFK", "NRT"], ["NRT", "JFK"]], the smallest-first walk goes JFK -> KUL and gets stuck, with the loop JFK -> NRT -> JFK still unused. The correct itinerary is ["JFK", "NRT", "JFK", "KUL"].

One trace: ten tickets, one dead end, two loops

The board uses 10 tickets and 8 airports. The airport JFK is the start, because it has one more ticket out than in. The airport KUL is a dead end with no ticket out. The tickets are JFK -> ATL, ATL -> ORD, ORD -> JFK, JFK -> KUL, JFK -> NRT, NRT -> SFO, SFO -> ICN, ICN -> NRT, NRT -> LAX and LAX -> JFK.

The walk goes JFK, ATL, ORD, JFK. At JFK the smallest unused ticket goes to KUL, so the walk goes there and gets stuck. KUL moves to path. This is the first dead end, and KUL is the last stop.

The loop backs up to JFK, which still has a ticket to NRT. The walk goes NRT, LAX, JFK and gets stuck again, because JFK has no ticket left. JFK and LAX move to path. At NRT, the ticket to SFO is still unused. The walk goes SFO, ICN, NRT, and then everything moves to path in turn.

The final path is [KUL, JFK, LAX, NRT, ICN, SFO, NRT, JFK, ORD, ATL, JFK]. Reversed, it is [JFK, ATL, ORD, JFK, NRT, SFO, ICN, NRT, LAX, JFK, KUL]. The board uses 10 steps that delete a ticket and 11 steps that move an airport to path.

The traps: writing on arrival, the start, the sort and the reversal

Do not write an airport into the answer when you first reach it. On the board tickets, the greedy walk goes JFK, ATL, ORD, JFK, KUL and stops, because it is stuck at KUL. It used 4 of the 10 tickets. The blueprint writes an airport only when its tickets are used up, so the loops are not lost.

The start must be the airport with balance[x] == 1. For edges = [[1, 0], [0, 2]], the correct walk is [1, 0, 2]. Starting at the smallest node, 0, gives [0, 2] and leaves the ticket 1 -> 0 unused. Only when every airport is balanced is the path a circuit, and then it starts at min(graph).

The tickets are sorted with reverse=True, so pop() takes the smallest neighbour in O(1). With an ascending sort, pop() takes the largest. On the board tickets, the ascending sort gives [JFK, NRT, SFO, ICN, NRT, LAX, JFK, ATL, ORD, JFK, KUL]. This is a valid walk, but the correct answer goes JFK, ATL and not JFK, NRT.

The last line is path[::-1]. Airports reach path last-first. Without the reversal, the example [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]] returns [SJC, SFO, LHR, MUC, JFK] and not [JFK, MUC, LHR, SFO, SJC].

The cost, and the neighbours

Sorting the tickets costs O(E log E). The walk then deletes each ticket once and moves each airport to path once, which is O(E). The total is O(E log E) time. graph, balance, stack and path hold O(E) entries, so the space is O(E).

On the board, 10 tickets give 11 airports in the answer. Each ticket is deleted in one step, and each of the 11 stops reaches path in one step.

The same walk solves Valid Arrangement of Pairs (LC 2097), where the pairs are the edges and any valid order is accepted. Cracking the Safe (LC 753) uses it on a graph whose nodes are strings, and the walk gives the shortest string that contains every combination.

The Topological Sort card solves a different question. In Course Schedule II (LC 210), every node appears once, after everything it depends on. That is an order of the nodes. Here every edge is used once, and a node can appear many times. If you only need to know which nodes are joined, a plain search is enough, as in Number of Provinces (LC 547).

60-Second Pitch

"Every ticket must be used once, so this is an Eulerian path, and I use Hierholzer's algorithm. I sort the tickets in reverse and keep them in a list for each airport, so pop() takes the smallest destination. I start at the airport with one more ticket out than in, or at the smallest airport if all are balanced. I keep a stack. If the top airport has an unused ticket, I delete it and push the destination. If it has none, I move the airport to path. An airport goes into path only when its tickets are used up. So a dead end is placed last, and a loop that the walk skipped is walked when the stack backs up to its airport. At the end I return path reversed. Sorting costs O(E log E), and the walk costs O(E). The space is O(E)."

Common Questions

Why does the walk get stuck only at the end airport?

Every airport except the start and the end has as many tickets out as in. Each entry into such an airport uses one ticket in, so one ticket out is still unused. The start has one extra ticket out. So the only airport where the walk can run out of tickets is the end.

Why do you add an airport to path when it is stuck, and not when it is reached?

The first stuck airport is the last stop. A loop that the walk skipped starts and ends at an airport that is still on stack. If airports were written on arrival, the dead end would be written before the loop, and the loop would be lost. With the stuck rule, the loop is walked and placed before the airports under it.

How do you get the smallest itinerary?

Sort the tickets in reverse and use pop(), so each airport always takes its smallest unused destination first. Hierholzer's algorithm with this rule returns the smallest valid walk for Reconstruct Itinerary (LC 332).

What are the time and space complexity?

O(E log E) time, because of the sort, and O(E) space. The walk itself deletes each ticket once and moves each airport to path once.

How do you know that a valid walk exists?

Check the counts. In a directed graph, an Eulerian path needs every airport balanced, except at most one with one more ticket out than in (the start) and one with one more ticket in than out (the end). The edges must also be joined in one piece. Reconstruct Itinerary (LC 332) promises that a valid itinerary exists.

SWE Reasoning

Spot it

  • Every ticket, domino, pair or transition must be used exactly once in one walk.
  • The question asks for the walk or the order of the stops, and not only whether it exists.
  • The wording says itinerary, arrangement, or the shortest string that contains every combination.
  • When two choices are possible, the smallest one must win, so the neighbours are sorted.
  • Trying every order of the edges would take O(E!) time, which is far too slow.

The invariant, step by step

  1. The tickets are sorted in reverse order, so graph[u].pop() takes the smallest neighbour of u.
  2. start is the airport with balance[x] == 1. If every airport is balanced, start is the smallest airport.
  3. The loop looks at node = stack[-1]. If graph[node] is not empty, it deletes the smallest ticket with pop() and pushes the airport it leads to.
  4. If graph[node] is empty, the loop moves node from stack to path. No edge leaves this airport any more, so it is final in the rest of the walk.
  5. stack is always a walk from start over used tickets, and path holds the final part of the answer, last-first.
  6. When stack is empty, every ticket was used. path[::-1] is the itinerary.

Traps

  • Writing an airport into the answer when it is first reached. The walk can enter the final dead end before a loop is used, and the loop is lost. Add an airport to path only when its tickets run out.
  • Starting at the smallest airport. A walk that is not a circuit must start at the airport with balance[x] == 1.
  • Sorting ascending. Then pop() takes the largest neighbour, and the walk is not the smallest one. Sort with reverse=True.
  • Using pop(0) for the smallest neighbour. It moves every other item, so each step costs O(E). Sort in reverse and use pop().
  • Returning path without reversing it. Airports reach path last-first. Return path[::-1].

Practice

Canonical LeetCode:Reconstruct Itinerary (LC 332)Valid Arrangement of Pairs (LC 2097)Cracking the Safe (LC 753)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•