title: Reconstruct Itinerary
slug: reconstruct-itinerary
difficulty: hard
leetcode_id: 332
leetcode_url: https://leetcode.com/problems/reconstruct-itinerary/
categories:
  - graphs
patterns:
  - dfs

description: |
  You are given a list of airline `tickets` where `tickets[i] = [from_i, to_i]` represent the departure and the arrival airports of one flight. Reconstruct the itinerary in order and return it.

  All of the tickets belong to a man who departs from `"JFK"`, thus, the itinerary must begin with `"JFK"`. If there are multiple valid itineraries, you should return the itinerary that has the smallest lexical order when read as a single string.

  - For example, the itinerary `["JFK", "LGA"]` has a smaller lexical order than `["JFK", "LGB"]`.

  You may assume all tickets form at least one valid itinerary. You must use all the tickets once and only once.

constraints: |
  - `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
  - `from_i != to_i`

examples:
  - input: 'tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]]'
    output: '["JFK","MUC","LHR","SFO","SJC"]'
    explanation: "Starting from JFK, we fly to MUC, then LHR, then SFO, and finally SJC. This uses all 4 tickets exactly once."
  - input: 'tickets = [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]'
    output: '["JFK","ATL","JFK","SFO","ATL","SFO"]'
    explanation: "Another valid reconstruction is [\"JFK\",\"SFO\",\"ATL\",\"JFK\",\"ATL\",\"SFO\"] but it is larger in lexical order. We choose ATL first from JFK because 'ATL' < 'SFO' lexicographically."

explanation:
  intuition: |
    Think of this problem as finding a path through a directed graph where each ticket is an edge. The challenge is that you must use **every edge exactly once** — this is known as an **Eulerian path**.

    Imagine you're a traveler at JFK airport with a stack of plane tickets. You need to use every single ticket exactly once and end up with a valid journey. The tricky part: if there are multiple choices at any airport, you must pick the lexicographically smallest destination to get the "smallest" itinerary.

    The key insight is that we can use **Hierholzer's algorithm** — a classic algorithm for finding Eulerian paths. The idea is:

    1. Always greedily visit the smallest lexicographic neighbor first
    2. When you reach a "dead end" (an airport with no more outgoing tickets), that airport must be the *last* stop in the final path
    3. Backtrack and add airports to the result in **reverse order**

    Why does this work? When we hit a dead end, we know that airport has no more outgoing flights — so it must come at the end of the itinerary. By building the result in reverse as we backtrack, we naturally construct the correct path.

  approach: |
    We solve this using **DFS with Hierholzer's Algorithm**:

    **Step 1: Build an adjacency list**

    - Create a graph where each airport maps to a sorted list of destinations
    - Use a data structure that allows efficient removal of the smallest element (like a heap or sorted list)
    - Sorting ensures we always pick the lexicographically smallest destination first

    &nbsp;

    **Step 2: Perform DFS from JFK**

    - Start at "JFK"
    - While there are outgoing flights from the current airport:
      - Pop the smallest destination (this "uses" the ticket)
      - Recursively visit that destination
    - When no more flights remain from an airport, add it to the result

    &nbsp;

    **Step 3: Reverse the result**

    - Since we add airports when we backtrack (when stuck), the result is built in reverse order
    - Reverse it at the end to get the correct itinerary

    &nbsp;

    This greedy DFS approach works because the problem guarantees a valid Eulerian path exists. By always choosing the lexicographically smallest option and building the result from dead ends backward, we ensure the smallest valid itinerary.

  common_pitfalls:
    - title: Using Tickets Multiple Times
      description: |
        A naive DFS might visit the same edge (ticket) more than once if you don't track which tickets have been used.

        The key is to **remove** or mark tickets as used. Using a data structure like a list with `pop()` naturally handles this — once a destination is popped, that ticket is consumed.
      wrong_approach: "Visiting neighbors without removing them"
      correct_approach: "Pop destinations from the adjacency list to consume tickets"

    - title: Not Handling Backtracking Correctly
      description: |
        If you greedily go to the smallest destination and hit a dead end with unused tickets elsewhere, you might get stuck.

        Example: With tickets `[["JFK","KUL"],["JFK","NRT"],["NRT","JFK"]]`, greedily going JFK -> KUL first leads to a dead end, leaving tickets unused.

        Hierholzer's algorithm handles this: when stuck at KUL, we add it to the result and backtrack. The algorithm naturally explores other branches and reconstructs the correct path.
      wrong_approach: "Simple greedy without proper backtracking"
      correct_approach: "Hierholzer's algorithm with post-order insertion"

    - title: Forgetting Lexicographic Order
      description: |
        The problem requires the smallest lexical order. If you don't sort destinations before exploring, you might find *a* valid path but not *the* lexicographically smallest one.

        For example, with `["JFK","ATL"]` and `["JFK","SFO"]` available, you must try ATL first because `"ATL" < "SFO"`.
      wrong_approach: "Iterating destinations in arbitrary order"
      correct_approach: "Sort destinations and always pick the smallest first"

  key_takeaways:
    - "**Eulerian path pattern**: When you need to traverse every edge exactly once, think Hierholzer's algorithm"
    - "**Post-order construction**: Build the result during backtracking, then reverse — this naturally handles complex graph structures"
    - "**Lexicographic ordering**: Use sorted data structures (heaps or sorted lists) to greedily pick the smallest option at each step"
    - "**Graph as adjacency list**: Modelling tickets as directed edges enables graph traversal algorithms"

  time_complexity: "O(E log E). We process each edge once, but sorting destinations takes O(E log E) where E is the number of tickets."
  space_complexity: "O(E). We store all edges in the adjacency list, plus O(E) for the recursion stack in the worst case."

solutions:
  - approach_name: DFS with Hierholzer's Algorithm
    is_optimal: true
    code: |
      from collections import defaultdict

      def find_itinerary(tickets: list[list[str]]) -> list[str]:
          # Build adjacency list with sorted destinations (reversed for efficient popping)
          graph = defaultdict(list)
          for src, dst in sorted(tickets, reverse=True):
              graph[src].append(dst)

          result = []

          def dfs(airport: str) -> None:
              # Visit all destinations from this airport in lexicographic order
              while graph[airport]:
                  # Pop smallest destination (from end since we sorted in reverse)
                  next_airport = graph[airport].pop()
                  dfs(next_airport)
              # Add to result when no more outgoing flights (dead end)
              result.append(airport)

          dfs("JFK")

          # Result is built in reverse order, so reverse it
          return result[::-1]
    explanation: |
      **Time Complexity:** O(E log E) — Sorting tickets dominates. The DFS visits each edge once.

      **Space Complexity:** O(E) — Adjacency list stores all edges; recursion stack can be O(E) deep.

      We sort tickets in reverse order so that popping from the list (O(1)) gives us the smallest destination. The DFS explores all paths, adding airports to the result when backtracking from dead ends. Reversing at the end gives the correct itinerary.

  - approach_name: DFS with Heap
    is_optimal: false
    code: |
      from collections import defaultdict
      import heapq

      def find_itinerary(tickets: list[list[str]]) -> list[str]:
          # Build adjacency list using min-heaps for lexicographic order
          graph = defaultdict(list)
          for src, dst in tickets:
              heapq.heappush(graph[src], dst)

          result = []

          def dfs(airport: str) -> None:
              # Visit all destinations in lexicographic order
              while graph[airport]:
                  # Pop smallest destination from heap
                  next_airport = heapq.heappop(graph[airport])
                  dfs(next_airport)
              # Add to result when stuck
              result.append(airport)

          dfs("JFK")

          return result[::-1]
    explanation: |
      **Time Complexity:** O(E log E) — Each heap push/pop is O(log E), done E times.

      **Space Complexity:** O(E) — Same as above.

      This variation uses heaps instead of pre-sorted lists. The logic is identical to Hierholzer's algorithm, but `heappop()` directly gives the smallest destination. Both approaches have the same complexity; the sorted list version has slightly better constants due to cache locality.

  - approach_name: Iterative with Stack
    is_optimal: false
    code: |
      from collections import defaultdict

      def find_itinerary(tickets: list[list[str]]) -> list[str]:
          # Build adjacency list with sorted destinations
          graph = defaultdict(list)
          for src, dst in sorted(tickets, reverse=True):
              graph[src].append(dst)

          stack = ["JFK"]
          result = []

          while stack:
              # Peek at current airport
              airport = stack[-1]

              if graph[airport]:
                  # More destinations available: continue exploring
                  stack.append(graph[airport].pop())
              else:
                  # Dead end: add to result and backtrack
                  result.append(stack.pop())

          return result[::-1]
    explanation: |
      **Time Complexity:** O(E log E) — Same as the recursive version.

      **Space Complexity:** O(E) — Explicit stack replaces recursion.

      This iterative version avoids recursion, which can be useful for very deep graphs to prevent stack overflow. The logic mirrors the recursive DFS: explore while destinations exist, add to result when stuck, and reverse at the end.
