Reconstruct Itinerary

Medium Backtracking

Problem

Given a list of airline tickets represented as pairs of departure and arrival airports [from, to], reconstruct the itinerary in order such that all tickets are used exactly once and the itinerary starts from 'JFK'. If multiple valid itineraries exist, return the itinerary that is lexicographically smallest.

  • 1 ≤ tickets.length ≤ 300
  • tickets[i].length == 2
  • from_i.length == 3
  • to_i.length == 3
  • from_i and to_i consist of uppercase English letters.

Example

Input: tickets = [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]]
Output: ["JFK", "MUC", "LHR", "SFO", "SJC"]

Starting from 'JFK', the algorithm explores destinations in lexicographical order. It first chooses 'MUC' from 'JFK', then 'LHR' from 'MUC', followed by 'SFO' from 'LHR', and finally 'SJC' from 'SFO'. This path uses all tickets exactly once and is lexicographically smallest.

Approach

Straightforward Solution

A brute-force approach tries all permutations of tickets to find a valid itinerary, which is factorial in time and infeasible for large inputs.

Core Observation

The problem requires finding a path that uses all edges (tickets) exactly once in a directed graph, starting from a fixed node 'JFK'. This is a classic Eulerian path problem with the added constraint of lexicographical order.

Path to Optimal

Recognizing the problem as finding an Eulerian path allows the use of Hierholzer's algorithm or backtracking with pruning. Sorting the adjacency lists lexicographically ensures the lexicographically smallest path is found first. Backtracking tries each edge in order, removing it temporarily to avoid reuse, and restores it if the path does not lead to a solution.

Optimal Approach

Build an adjacency list mapping each source to a lexicographically sorted list of destinations. Use backtracking starting from 'JFK', at each step choosing the next lexicographically smallest destination, removing the edge from the adjacency list, and recursively continuing. If the path uses all tickets, return true to stop further search. Restore edges on backtrack to explore alternatives if needed.

Time

O(E!) in the worst case

Backtracking explores permutations of edges, but sorting and pruning reduce the search space significantly. The worst case is factorial in the number of tickets due to permutations, but pruning stops early once a valid path is found.

Space

O(V + E)

The adjacency list stores all edges, and recursion stack can go as deep as the number of tickets (edges). Auxiliary space is proportional to vertices and edges.

Pattern Spotlight

Backtracking (State Restoration with Lexicographical Ordering)

When searching for a path that must use all edges exactly once with lexicographical constraints, use backtracking that tries edges in sorted order, removing edges as they are used and restoring them upon backtracking to maintain correct state and enable pruning.

Solution

1import collections
2
3class Solution:
4 def findItinerary(self, tickets: list[list[str]]) -> list[str]:
5 adj = {src: [] for src, dst in tickets}
6 tickets.sort()
7 for src, dst in tickets:
8 adj[src].append(dst)
9
10 res = ["JFK"]
11 def dfs(src):
12 if len(res) == len(tickets) + 1:
13 return True
14 if src not in adj:
15 return False
16
17 temp = list(adj[src])
18 for i, v in enumerate(temp):
19 adj[src].pop(i)
20 res.append(v)
21 if dfs(v):
22 return True
23 adj[src].insert(i, v)
24 res.pop()
25 return False
26
27 dfs("JFK")
28 return res

Step-by-Step Solution

1

Build Lexicographically Sorted Adjacency List

5adj = {src: [] for src, dst in tickets}
6tickets.sort()
7for src, dst in tickets:
8 adj[src].append(dst)

Objective

To construct a graph representation where each source airport maps to a sorted list of destination airports, enabling lexicographical traversal.

Key Insight

Sorting the tickets before building the adjacency list ensures that destinations are appended in lexicographical order. This ordering is critical because the backtracking algorithm relies on exploring edges in sorted order to find the lexicographically smallest itinerary first.

Interview Quick-Check

Core Logic

Sorting tickets upfront guarantees adjacency lists are lexicographically ordered, enabling efficient backtracking without additional sorting.

Common Pitfalls & Bugs

Failing to sort the tickets or adjacency lists can lead to incorrect or non-lexicographically minimal itineraries.

2

Initialize Result and Define Backtracking DFS

10res = ["JFK"]
11def dfs(src):
12 if len(res) == len(tickets) + 1:
13 return True
14 if src not in adj:
15 return False

Objective

To prepare the result list starting from 'JFK' and define a recursive DFS function that attempts to build a valid itinerary using all tickets.

Key Insight

Starting the result with 'JFK' sets the fixed starting point. The DFS function attempts to extend the itinerary by exploring all possible next destinations from the current airport. It uses the length of the result to determine when all tickets have been used, enabling early termination.

Interview Quick-Check

Core Logic

The DFS uses the length of the current itinerary to detect completion, ensuring all tickets are used exactly once.

State & Boundaries

The base case returns true when the itinerary length equals the number of tickets plus one, indicating a complete path.

3

Explore Next Destinations with Backtracking and State Restoration

17temp = list(adj[src])
18for i, v in enumerate(temp):
19 adj[src].pop(i)
20 res.append(v)
21 if dfs(v):
22 return True
23 adj[src].insert(i, v)
24 res.pop()
25return False

Objective

To recursively try each possible next destination from the current airport, removing the edge to prevent reuse and restoring it if the path does not lead to a solution.

Key Insight

Copying the adjacency list for iteration avoids issues with modifying the list during traversal. Removing the edge before recursion prevents reuse of the same ticket. If recursion fails, restoring the edge and popping the last airport from the result restores the state for alternative paths. This careful state management is essential for correctness and efficiency.

Interview Quick-Check

Core Logic

Backtracking tries each edge in lexicographical order, removing it to prevent reuse and restoring it on failure to explore alternatives.

Common Pitfalls & Bugs

Modifying the adjacency list while iterating without copying can cause skipped edges or runtime errors.

State & Boundaries

Returning true immediately upon finding a valid complete itinerary prunes the search space, improving efficiency.

4

Invoke DFS from 'JFK' and Return Final Itinerary

27dfs("JFK")
28return res

Objective

To start the backtracking search from 'JFK' and return the constructed itinerary after successful completion.

Key Insight

The initial call to DFS triggers the recursive exploration of all possible itineraries. Since the algorithm returns as soon as a valid itinerary is found, the final result is guaranteed to be lexicographically smallest due to the sorted adjacency lists.

Interview Quick-Check

Core Logic

Starting DFS from 'JFK' ensures the itinerary begins at the required airport.

Complexity

The early return upon success prevents unnecessary exploration of all permutations, improving practical runtime.

Line Analysis

This solution has 7 Critical lines interviewers watch for.

Line 19 Critical
adj[src].pop(i)

Remove the destination from the original adjacency list to mark the ticket as used.

Removing the edge prevents reuse of the same ticket, ensuring each ticket is used exactly once in the itinerary.

Line 23 Critical
adj[src].insert(i, v)

Restore the removed destination to the adjacency list upon backtracking.

Restoring the edge allows alternative paths to be explored, maintaining correct state for backtracking.

Line 24 Critical
res.pop()

Remove the last added destination from the itinerary to backtrack.

Removing the last airport restores the path state, enabling exploration of other possible itineraries.

Line 6 Critical
tickets.sort()

Sort the tickets lexicographically by source and destination.

Sorting ensures that when destinations are appended to adjacency lists, they are in lexicographical order, which is crucial for finding the lexicographically smallest itinerary.

Line 12 Critical
if len(res) == len(tickets) + 1:

Check if the itinerary uses all tickets plus one airport.

This base case detects when a complete valid itinerary has been constructed, enabling early termination of recursion.

Line 17 Critical
temp = list(adj[src])

Create a copy of the current airport's adjacency list for safe iteration.

Copying prevents issues from modifying the adjacency list during iteration, preserving correct traversal order and state.

Line 22 Critical
return True

Return true immediately if a complete itinerary is found.

Early return prunes the search space, avoiding unnecessary exploration once a valid path is discovered.

Line 10 Core
res = ["JFK"]

Initialize the result itinerary starting with 'JFK'.

Starting the itinerary with 'JFK' sets the fixed starting point as required by the problem, forming the base of the path to be extended.

Line 13 Core
return True

Return true to signal a complete itinerary has been found.

Returning true propagates success up the recursion stack, preventing further unnecessary exploration.

Line 20 Core
res.append(v)

Append the chosen destination to the current itinerary.

Adding the destination extends the current path, representing the choice of this ticket in the itinerary.

Line 21 Core
if dfs(v):

Recursively attempt to build the itinerary from the chosen destination.

Recursion explores deeper paths, attempting to use all remaining tickets from the new airport.

Line 25 Core
return False

Return false if no valid itinerary is found from current airport.

Returning false signals the need to backtrack further and try different edges.

Line 5 Core
adj = {src: [] for src, dst in tickets}

Initialize adjacency list with empty lists for each source airport.

Creating a dictionary with keys as source airports and empty lists prepares the graph structure to store destinations, enabling efficient edge insertion.

Line 8 Core
adj[src].append(dst)

Append the destination airport to the source's adjacency list.

Appending destinations in sorted order maintains the lexicographical order required for the backtracking search.

Line 11 Core
def dfs(src):

Define the recursive DFS function to build the itinerary.

Encapsulating the backtracking logic in a helper function allows recursive exploration of all possible itineraries from a given airport.

Line 14 Core
if src not in adj:

Check if the current airport has no outgoing edges.

If no destinations remain from the current airport, the path cannot be extended, so this branch fails.

Line 15 Core
return False

Return false to indicate no valid path from current airport.

Returning false triggers backtracking to explore alternative paths.

Line 18 Core
for i, v in enumerate(temp):

Enumerate over the copied list of next destinations.

Enumerating with indices allows precise removal and restoration of edges during backtracking.

Line 27 Core
dfs("JFK")

Start the backtracking search from 'JFK'.

Initiating DFS from 'JFK' ensures the itinerary begins at the required starting airport.

Line 28 Core
return res

Return the constructed itinerary after successful backtracking.

Returning the result provides the final valid itinerary that uses all tickets exactly once and is lexicographically smallest.

Line 7 Standard
for src, dst in tickets:

Iterate over each ticket to build the adjacency list.

This loop populates the adjacency list by mapping each source airport to its destinations, establishing the graph representation of the tickets.

Test Your Understanding

Why must edges be removed from the adjacency list when used and restored upon backtracking?

Removing edges prevents reuse of the same ticket, ensuring each ticket is used exactly once. Restoring edges upon backtracking allows exploring alternative paths, maintaining correctness of the search state.

Related Problems

Backtracking pattern

Don't just read it. Drill it.

Reconstruct Reconstruct Itinerary from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Reconstruct Itinerary drill

or drill a free problem