332. Reconstruct Itinerary
Problem Description
You need to reconstruct a travel itinerary from a collection of airline tickets. Each ticket is represented as [from, to]
where from
is the departure airport and to
is the arrival airport.
The key requirements are:
- Starting Point: The journey must begin at "JFK" airport
- Use All Tickets: Every ticket must be used exactly once - no ticket can be skipped or used multiple times
- Valid Path: The tickets must form a continuous path where each destination becomes the next departure point
- Lexicographical Order: If multiple valid itineraries exist, return the one that comes first alphabetically when read as a sequence
For example, if you have tickets [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
, you need to find a path starting from "JFK" that uses all 5 tickets exactly once. The result would be ["JFK","ATL","JFK","SFO","ATL","SFO"]
.
When comparing itineraries lexicographically, earlier airports in alphabetical order take precedence. For instance, ["JFK", "LGA"]
comes before ["JFK", "LGB"]
because "LGA" < "LGB" alphabetically.
The problem guarantees that at least one valid itinerary exists, so you don't need to handle cases where it's impossible to use all tickets.
This is fundamentally an Eulerian path problem - finding a path through a directed graph that visits every edge exactly once, starting from a specific vertex ("JFK"), with the additional constraint of choosing the lexicographically smallest path when multiple options exist.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem involves airports (nodes) and flights (directed edges) between them. Each ticket represents a directed edge from one airport to another.
Is it a tree?
- No: The graph can have cycles (you can return to previously visited airports) and multiple paths between nodes. For example, you might fly from JFK to ATL and later from SFO back to JFK.
Is the problem related to directed acyclic graphs?
- No: The graph can contain cycles. The itinerary might involve returning to previously visited airports, creating cycles in the graph.
Is the problem related to shortest paths?
- No: We're not looking for the shortest path. We need to use ALL edges (tickets) exactly once, which is an Eulerian path problem.
Does the problem involve connectivity?
- No: We're not checking if nodes are connected or finding connected components. We need to traverse all edges exactly once.
Does the problem have small constraints?
- Yes: While not explicitly stated in this problem description, airline ticket problems typically have manageable constraints that allow for recursive exploration.
DFS/backtracking?
- Yes: We need to explore paths recursively, potentially backtracking when we reach dead ends, to find the valid itinerary that uses all tickets exactly once.
Conclusion: The flowchart suggests using a DFS/backtracking approach. The solution uses DFS to traverse the graph, visiting each edge (ticket) exactly once. The algorithm maintains the lexicographical order by sorting tickets in reverse order initially and using a stack-based approach (via recursion) to build the path.
Intuition
The key insight is recognizing this as an Eulerian path problem - we need to visit every edge (ticket) exactly once. But how do we ensure we get the lexicographically smallest path?
Think about what happens when we're at an airport with multiple outgoing flights. If we always choose the lexicographically smallest destination first, we might get stuck! For example, if from "JFK" we can go to "ATL" or "SFO", choosing "ATL" first might lead us down a path where we can't use all tickets.
The clever trick is to reverse our thinking. Instead of building the path forward and potentially getting stuck, we can build it backward using post-order DFS traversal. Here's why this works:
-
Sort destinations in reverse order: For each airport, we arrange its destinations from largest to smallest lexicographically. This seems counterintuitive at first!
-
Use DFS with a greedy approach: We keep visiting airports, always taking the "last" destination from our sorted list (which due to reverse sorting is actually the lexicographically smallest available).
-
Build the path in reverse: The magic happens when we add airports to our result. We only add an airport to our answer AFTER we've visited all destinations reachable from it (post-order traversal). This ensures we don't get stuck.
-
Why post-order works: When we reach a dead-end (an airport with no more unused outgoing tickets), we add it to our result and backtrack. The airports get added in reverse order of visitation. By reversing the final result, we get the correct forward path.
The algorithm essentially finds the path by exhausting all edges, prioritizing lexicographically smaller choices (via the reverse sort trick), and using the call stack to naturally handle the backtracking. It's like solving a maze by always taking the leftmost path, but recording your steps as you backtrack from dead ends, then reversing that recording to get the actual path from start to finish.
Solution Approach
The solution implements Hierholzer's algorithm for finding an Eulerian path. Let's break down the implementation step by step:
1. Data Structure Setup:
g = defaultdict(list)
for f, t in sorted(tickets, reverse=True):
g[f].append(t)
- We use a
defaultdict(list)
to represent the graph as an adjacency list - Each airport maps to a list of destination airports
- Critical trick: We sort tickets in reverse order before building the graph. This ensures that when we pop from the end of each list later, we're actually getting the lexicographically smallest destination
2. DFS Implementation:
def dfs(f: str):
while g[f]:
dfs(g[f].pop())
ans.append(f)
- The DFS function takes a departure airport
f
- While there are still unused tickets from this airport:
- Pop the last destination (which is lexicographically smallest due to reverse sorting)
- Recursively visit that destination
- After all destinations are exhausted, add the current airport to the answer
3. Building the Result:
ans = [] dfs("JFK") return ans[::-1]
- Start DFS from "JFK" as required
- The
ans
list gets populated in post-order (airports are added after visiting all their destinations) - Reverse the final answer to get the correct forward path
Why This Works:
The algorithm exploits the property of Eulerian paths: if we keep following edges and removing them as we go, we'll eventually use all edges exactly once. The post-order traversal ensures that:
- Dead-end airports (with no outgoing tickets) are added to the result first
- Airports that lead to dead-ends are added next
- The starting airport "JFK" is added last
When we reverse this, we get a valid path from "JFK" that uses all tickets exactly once.
Time Complexity: O(E log E)
where E is the number of tickets (for sorting)
Space Complexity: O(E)
for storing the graph and recursion stack
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through a small example with tickets: [["JFK","B"],["B","JFK"],["JFK","A"],["A","JFK"]]
Step 1: Sort tickets in reverse order
After sorting in reverse: [["JFK","B"],["JFK","A"],["B","JFK"],["A","JFK"]]
Step 2: Build the graph
g = { "JFK": ["B", "A"] // Note: B comes before A (we'll pop from the end) "B": ["JFK"] "A": ["JFK"] }
Step 3: DFS traversal starting from JFK
Let's trace the DFS calls:
-
dfs("JFK")
- JFK has destinations ["B", "A"]- Pop "A" (last element), so we visit A first
- Call
dfs("A")
-
dfs("A")
- A has destination ["JFK"]- Pop "JFK", call
dfs("JFK")
- Pop "JFK", call
-
dfs("JFK")
again - JFK now has ["B"] left- Pop "B", call
dfs("B")
- Pop "B", call
-
dfs("B")
- B has destination ["JFK"]- Pop "JFK", call
dfs("JFK")
- Pop "JFK", call
-
dfs("JFK")
again - JFK has no more destinations- No more tickets, append "JFK" to ans
- ans = ["JFK"]
-
Back to
dfs("B")
- finished with while loop- Append "B" to ans
- ans = ["JFK", "B"]
-
Back to
dfs("JFK")
from step 3 - finished with while loop- Append "JFK" to ans
- ans = ["JFK", "B", "JFK"]
-
Back to
dfs("A")
- finished with while loop- Append "A" to ans
- ans = ["JFK", "B", "JFK", "A"]
-
Back to initial
dfs("JFK")
- finished with while loop- Append "JFK" to ans
- ans = ["JFK", "B", "JFK", "A", "JFK"]
Step 4: Reverse the result
Final answer: ["JFK", "A", "JFK", "B", "JFK"]
This gives us the lexicographically smallest path that uses all tickets exactly once. Notice how by popping from the end (which gives us the smallest destination due to reverse sorting) and using post-order traversal, we naturally find the correct path without getting stuck!
Solution Implementation
1from collections import defaultdict
2from typing import List
3
4class Solution:
5 def findItinerary(self, tickets: List[List[str]]) -> List[str]:
6 # Build adjacency list (graph) from tickets
7 # Sort tickets in reverse order so that when we pop from the list,
8 # we get the lexicographically smallest destination first
9 graph = defaultdict(list)
10 for from_airport, to_airport in sorted(tickets, reverse=True):
11 graph[from_airport].append(to_airport)
12
13 # Result list to store the itinerary in reverse order
14 result = []
15
16 def dfs(current_airport: str) -> None:
17 """
18 Perform depth-first search using Hierholzer's algorithm
19 to find Eulerian path.
20
21 Args:
22 current_airport: Current airport code
23 """
24 # Visit all destinations from current airport
25 while graph[current_airport]:
26 # Pop the lexicographically smallest destination
27 # (since list is in reverse order, pop gives smallest)
28 next_airport = graph[current_airport].pop()
29 dfs(next_airport)
30
31 # Add current airport to result after visiting all its destinations
32 # This ensures we build the path from end to start
33 result.append(current_airport)
34
35 # Start DFS from JFK airport
36 dfs("JFK")
37
38 # Reverse the result to get the correct order of itinerary
39 return result[::-1]
40
1class Solution {
2 // Graph adjacency list: maps each airport to list of destinations
3 private Map<String, List<String>> adjacencyList = new HashMap<>();
4 // Result list to store the final itinerary
5 private List<String> itinerary = new ArrayList<>();
6
7 public List<String> findItinerary(List<List<String>> tickets) {
8 // Sort tickets in reverse lexicographical order by destination
9 // This ensures when we pop from the end, we get lexicographically smallest
10 Collections.sort(tickets, (ticket1, ticket2) ->
11 ticket2.get(1).compareTo(ticket1.get(1)));
12
13 // Build the graph from tickets
14 for (List<String> ticket : tickets) {
15 String departure = ticket.get(0);
16 String arrival = ticket.get(1);
17 // Add arrival airport to the list of destinations from departure airport
18 adjacencyList.computeIfAbsent(departure, k -> new ArrayList<>())
19 .add(arrival);
20 }
21
22 // Start DFS traversal from JFK airport
23 dfs("JFK");
24
25 // Reverse the result since we build it in reverse order (Hierholzer's algorithm)
26 Collections.reverse(itinerary);
27 return itinerary;
28 }
29
30 /**
31 * Performs depth-first search using Hierholzer's algorithm for finding Eulerian path.
32 * Visits all edges exactly once and builds the itinerary in reverse order.
33 *
34 * @param currentAirport The current airport in the traversal
35 */
36 private void dfs(String currentAirport) {
37 // While current airport has unvisited destinations
38 while (adjacencyList.containsKey(currentAirport) &&
39 !adjacencyList.get(currentAirport).isEmpty()) {
40 // Get and remove the last destination (lexicographically smallest due to reverse sort)
41 List<String> destinations = adjacencyList.get(currentAirport);
42 String nextAirport = destinations.remove(destinations.size() - 1);
43 // Recursively visit the next airport
44 dfs(nextAirport);
45 }
46 // Add current airport to result after visiting all its destinations
47 itinerary.add(currentAirport);
48 }
49}
50
1class Solution {
2public:
3 vector<string> findItinerary(vector<vector<string>>& tickets) {
4 // Sort tickets in reverse order to ensure lexicographical order when popping from back
5 // Since we pop from back, larger strings should be at the back
6 sort(tickets.rbegin(), tickets.rend());
7
8 // Build adjacency list graph where key is departure airport and value is list of arrival airports
9 unordered_map<string, vector<string>> graph;
10 for (const auto& ticket : tickets) {
11 graph[ticket[0]].push_back(ticket[1]);
12 }
13
14 // Result vector to store the itinerary
15 vector<string> result;
16
17 // Recursive DFS function using Hierholzer's algorithm for finding Eulerian path
18 // We use C++23 deducing this for recursive lambda
19 auto dfs = [&](this auto&& dfs, string& currentAirport) -> void {
20 // Process all edges from current airport
21 while (!graph[currentAirport].empty()) {
22 // Get the next destination (lexicographically smallest due to reverse sort)
23 string nextAirport = graph[currentAirport].back();
24 graph[currentAirport].pop_back(); // Remove the edge after using it
25
26 // Recursively visit the next airport
27 dfs(nextAirport);
28 }
29 // Add current airport to result after processing all its edges (post-order)
30 result.emplace_back(currentAirport);
31 };
32
33 // Start the journey from JFK airport
34 string startAirport = "JFK";
35 dfs(startAirport);
36
37 // Reverse the result since we added airports in post-order
38 reverse(result.begin(), result.end());
39
40 return result;
41 }
42};
43
1/**
2 * Finds a valid itinerary that uses all tickets exactly once, starting from JFK.
3 * Uses Hierholzer's algorithm to find an Eulerian path in the graph.
4 * @param tickets - Array of ticket pairs where each pair is [from, to]
5 * @returns Array of airports representing the itinerary
6 */
7function findItinerary(tickets: string[][]): string[] {
8 // Build adjacency list to represent the graph of flights
9 const flightGraph: Record<string, string[]> = {};
10
11 // Sort tickets in reverse lexicographical order by destination
12 // This ensures we visit lexicographically smaller destinations first when popping
13 tickets.sort((ticketA, ticketB) => ticketB[1].localeCompare(ticketA[1]));
14
15 // Populate the adjacency list with destinations for each departure airport
16 for (const [departure, destination] of tickets) {
17 if (!flightGraph[departure]) {
18 flightGraph[departure] = [];
19 }
20 flightGraph[departure].push(destination);
21 }
22
23 // Result array to store the final itinerary
24 const itinerary: string[] = [];
25
26 /**
27 * Depth-first search to traverse all edges (tickets) exactly once
28 * @param currentAirport - Current airport in the traversal
29 */
30 const traverseFlights = (currentAirport: string): void => {
31 // Continue while there are unused tickets from current airport
32 while (flightGraph[currentAirport] && flightGraph[currentAirport].length > 0) {
33 // Pop the last destination (lexicographically smallest due to reverse sort)
34 const nextAirport = flightGraph[currentAirport].pop()!;
35 // Recursively visit the next airport
36 traverseFlights(nextAirport);
37 }
38 // Add current airport to itinerary after visiting all destinations
39 itinerary.push(currentAirport);
40 };
41
42 // Start the traversal from JFK airport
43 traverseFlights('JFK');
44
45 // Reverse the itinerary since we built it backwards using post-order traversal
46 return itinerary.reverse();
47}
48
Time and Space Complexity
Time Complexity: O(m Γ log m)
The time complexity is dominated by two main operations:
- Sorting the tickets: The
sorted(tickets, reverse=True)
operation takesO(m Γ log m)
time, wherem
is the number of tickets (edges). - DFS traversal: The DFS visits each edge exactly once. Since we pop each destination from the adjacency list, each edge is processed once, taking
O(m)
time in total.
Therefore, the overall time complexity is O(m Γ log m) + O(m) = O(m Γ log m)
.
Space Complexity: O(m)
The space complexity consists of:
- Graph storage (
g
): The defaultdict stores all edges, requiringO(m)
space to store all destinations. - Answer list (
ans
): The answer list will eventually containm + 1
airports (vertices), which isO(m)
space. - Recursion stack: In the worst case, the DFS recursion can go as deep as
m
levels (a linear path through all edges), requiringO(m)
stack space.
Therefore, the overall space complexity is O(m) + O(m) + O(m) = O(m)
.
Common Pitfalls
1. Incorrect Sorting Direction
One of the most common mistakes is sorting the tickets in ascending order instead of reverse order:
Wrong Approach:
# This seems intuitive but is incorrect!
for f, t in sorted(tickets): # Ascending order
graph[f].append(t)
Why it fails: When you pop from the end of a list sorted in ascending order, you get the lexicographically largest destination, not the smallest. This produces an incorrect itinerary.
Correct Approach:
# Sort in reverse so pop() gives smallest
for f, t in sorted(tickets, reverse=True):
graph[f].append(t)
2. Using a Priority Queue Instead of Sorted List
Some might try using a min-heap for lexicographical ordering:
Problematic Approach:
import heapq
graph = defaultdict(list)
for f, t in tickets:
heapq.heappush(graph[f], t)
# In DFS:
while graph[current]:
next_airport = heapq.heappop(graph[current]) # This works but...
Why it's inefficient: While this gives correct results, it has worse time complexity - O(E log E)
for heap operations during traversal, compared to O(E log E)
only for initial sorting in the optimal solution.
3. Building Path in Forward Direction
Attempting to build the itinerary directly in forward order:
Wrong Approach:
def dfs(current):
result.append(current) # Add before visiting destinations
while graph[current]:
next_airport = graph[current].pop()
dfs(next_airport)
Why it fails: This produces an incomplete path. The algorithm relies on post-order traversal to ensure all edges are used. Adding airports before exploring their destinations breaks the Eulerian path construction.
4. Not Handling Multiple Tickets Between Same Airports
Forgetting that there can be multiple tickets from airport A to airport B:
Wrong Approach:
graph = {} # Using set or single value for f, t in tickets: graph[f] = t # Overwrites previous ticket!
Correct Approach:
graph = defaultdict(list) # Must use list to store multiple tickets
for f, t in sorted(tickets, reverse=True):
graph[f].append(t) # Preserves all tickets
5. Misunderstanding the Lexicographical Requirement
Thinking you need to sort the entire path afterward:
Wrong Approach:
# Build any valid path first
result = build_any_path(tickets)
# Then try to sort it somehow
return sorted(result) # This doesn't work!
Why it fails: The lexicographical ordering must be considered during path construction, not after. The requirement is for the earliest lexicographical valid itinerary, not a sorted list of airports.
Solution: The algorithm handles this by always choosing the lexicographically smallest available destination at each step (via the reverse sort + pop combination).
Which of the following problems can be solved with backtracking (select multiple)
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donβt Miss This!