Blind Plus 100 Visualized

#91 ยท Advanced Graphs

Reconstruct Itinerary

Given airline tickets as [from, to] pairs, reconstruct the itinerary that starts at "JFK" and uses every ticket exactly once. If several valid itineraries exist, return the one with the smallest lexical order.

HardLeetCode
  • Current
  • On path / queued
  • Done
  • Cycle / blocked
step 1 / 1

Pointers

Variables

Hash map / table

Stack / queue

Foundations & data-structure selection

Directed multigraph (adjacency lists)

Airports are nodes, tickets are directed edges, and the same ticket can appear more than once. Using every ticket exactly once means walking an Eulerian path from JFK. Sorting each airport's destination list gives the lexical order.

Memory layout

A hash map from airport to a list of destinations, E entries in total.

OperationCost
build + sortO(E log E)

Stack (Hierholzer's algorithm)

Follow unused tickets greedily, always taking the smallest destination, until you get stuck. A stuck airport must be the end of the path, so it is written out and the walk resumes from the previous airport. Writing airports in post-order and reversing gives the Eulerian path.

Memory layout

An explicit stack of airports plus the output route, both at most E + 1 long.

OperationCost
each ticket flown onceO(E)

Why it fits

  • "Use every edge exactly once" is the definition of an Eulerian path, which Hierholzer's algorithm finds in linear time, without searching over orderings.
  • The greedy order (smallest destination first) can walk into a dead end, but post-order output places the dead end at the end of the route, so no backtracking is needed.
  • Popping from a descending-sorted list gives the smallest destination in O(1) and removes the ticket in the same step.

Why alternatives degrade

Greedy walk without post-order:
Taking the smallest destination can strand unused tickets behind a dead end (JFK โ†’ ABQ here).
Visited set of airports:
Airports are visited many times; it is the tickets that are used once.
Trying all permutations of tickets:
E! orders; backtracking prunes some, but can still be exponential.

Four approaches, from naive to production

1 ยท beginner

Backtracking over a sorted ticket list

time O(E!) worst casespace O(E)

Sort the tickets. From the current airport try each unused ticket in order, recurse, and undo the choice if the path cannot use all tickets.

vector<string> findItinerary(vector<vector<string>>& tickets) {
    vector<vector<string>> sorted(tickets);
    sort(sorted.begin(), sorted.end());
    vector<bool> used(sorted.size(), false);
    vector<string> path{"JFK"};
    function<bool()> dfs = [&]() -> bool {
        if (path.size() == sorted.size() + 1) return true;
        string from = path.back();
        for (size_t i = 0; i < sorted.size(); i++) {
            if (used[i] || sorted[i][0] != from) continue;
            used[i] = true;
            path.push_back(sorted[i][1]);
            if (dfs()) return true;
            path.pop_back();
            used[i] = false;
        }
        return false;
    };
    dfs();
    return path;
}

Flaws & bottlenecks

  • Scans the whole ticket list at every step to find tickets from the current airport.
  • Exponential when there are many dead ends.
  • Needs an extra used[] array.

Interviewer cross-questions

Why does sorting give the lexically smallest itinerary?

Say: The first complete path found while trying destinations in ascending order is the smallest, because each choice is the smallest that still works.

When does backtracking trigger?

Say: When the path reaches an airport with no unused outgoing ticket before all tickets are used: a dead end that is not the final airport.

How do you avoid scanning all tickets?

Say: Group them by origin in a map of sorted lists.

2 ยท intermediate

Backtracking with sorted adjacency lists

time O(E!) worst casespace O(E)

Build graph[from] = sorted destinations. Take a destination out of the list, recurse, and put it back at the same index on failure.

vector<string> findItinerary(vector<vector<string>>& tickets) {
    unordered_map<string, vector<string>> graph;
    for (auto& t : tickets) {
        graph[t[0]].push_back(t[1]);
    }
    for (auto& [airport, list] : graph) sort(list.begin(), list.end());
    int total = tickets.size();
    vector<string> path{"JFK"};
    function<bool(const string&)> dfs = [&](const string& from) -> bool {
        if ((int)path.size() == total + 1) return true;
        vector<string>& list = graph[from];
        for (size_t i = 0; i < list.size(); i++) {
            string to = list[i]; list.erase(list.begin() + i);
            path.push_back(to);
            if (dfs(to)) return true;
            path.pop_back();
            list.insert(list.begin() + i, to);
        }
        return false;
    };
    dfs("JFK");
    return path;
}

Flaws & bottlenecks

  • Array splice makes removal and reinsertion O(degree).
  • Still exponential on adversarial inputs.
  • Recursion depth equals the number of tickets.

Interviewer cross-questions

Why reinsert the ticket at the same index?

Say: To keep the list sorted so later attempts still try destinations in lexical order.

Is the first found itinerary the smallest?

Say: Yes, because destinations are tried in ascending order and the search is depth-first.

Can you avoid backtracking entirely?

Say: Yes: Hierholzer's algorithm uses post-order output so a dead end is simply written last.

3 ยท advanced

Hierholzer's algorithm (recursive)

time O(E log E)space O(E)

Sort each destination list descending. visit(airport) keeps flying the smallest unused ticket (list.pop()) recursively, and appends the airport to the route only when it has no tickets left. Reverse the route at the end.

vector<string> findItinerary(vector<vector<string>>& tickets) {
    unordered_map<string, vector<string>> graph;
    for (auto& t : tickets) {
        graph[t[0]].push_back(t[1]);
    }
    for (auto& [airport, list] : graph) sort(list.rbegin(), list.rend());
    vector<string> route;
    function<void(const string&)> visit = [&](const string& airport) {
        vector<string>& list = graph[airport];
        while (!list.empty()) { string to = list.back(); list.pop_back(); visit(to); }
        route.push_back(airport);
    };
    visit("JFK");
    reverse(route.begin(), route.end());
    return route;
}

Flaws & bottlenecks

  • Recursion depth can reach E (a long chain of tickets), which may overflow the stack.
  • The post-order trick is not intuitive.
  • Assumes a valid Eulerian path starting at JFK exists.

Interviewer cross-questions

Why is the route reversed?

Say: Airports are appended when they finish (post-order), so the path comes out last-to-first.

Why is the dead end placed correctly?

Say: A dead end can only be the final airport of the Eulerian path; it finishes first, so after reversing it is last.

What is the complexity?

Say: Sorting is O(E log E); every ticket is popped exactly once so the walk is O(E).

4 ยท expert

Iterative Hierholzer with validation

time O(E log E)space O(E)

Same algorithm with an explicit stack: peek the top, push the smallest unused destination if there is one, otherwise pop it to the route. Return the reversed route only if it contains tickets + 1 airports.

vector<string> findItinerary(vector<vector<string>>& tickets) {
    unordered_map<string, vector<string>> graph;
    for (auto& t : tickets) {
        graph[t[0]].push_back(t[1]);
    }
    for (auto& [airport, list] : graph) sort(list.rbegin(), list.rend());
    vector<string> stack{"JFK"};
    vector<string> route;
    while (!stack.empty()) {
        auto it = graph.find(stack.back());
        if (it != graph.end() && !it->second.empty()) {
            stack.push_back(it->second.back()); it->second.pop_back();
        } else {
            route.push_back(stack.back()); stack.pop_back();
        }
    }
    if (route.size() != tickets.size() + 1) return {};
    reverse(route.begin(), route.end());
    return route;
}

Flaws & bottlenecks

  • Still needs the O(E log E) sort for lexical order (a heap per airport has the same cost).
  • Input is assumed to start at JFK; a different start needs a parameter.
  • The length check detects only unusable ticket sets, not every invalid input.

Interviewer cross-questions

Why add the length check?

Say: If the tickets do not form a path from JFK that uses all of them, the route is shorter than tickets + 1; returning an empty list avoids an incorrect itinerary.

Why use an explicit stack?

Say: Itineraries can be 300 or more tickets long; an explicit stack removes any dependence on the recursion limit.

What are the edge cases?

Say: Duplicate tickets (keep both), a single ticket, self-loops (JFK โ†’ JFK), and airports that only appear as destinations.

Synchronized code walkthrough

Showing: Backtracking over a sorted ticket list โ€” shares the player above; click any step to jump.

vector<string> findItinerary(vector<vector<string>>& tickets) {
    vector<vector<string>> sorted(tickets);
    sort(sorted.begin(), sorted.end());
    vector<bool> used(sorted.size(), false);
    vector<string> path{"JFK"};
    function<bool()> dfs = [&]() -> bool {
        if (path.size() == sorted.size() + 1) return true;
        string from = path.back();
        for (size_t i = 0; i < sorted.size(); i++) {
            if (used[i] || sorted[i][0] != from) continue;
            used[i] = true;
            path.push_back(sorted[i][1]);
            if (dfs()) return true;
            path.pop_back();
            used[i] = false;
        }
        return false;
    };
    dfs();
    return path;
}