Reconstruct Itinerary
Reconstruct Itinerary is a hard graphs problem solved with the eulerian path (hierholzer) pattern.
The best approach, optimal (hierholzer's algorithm), runs in O(E log E) time and O(E) space.
Below are 2 approaches in Java, Python, C++ and JavaScript, from brute force (backtracking) up.
Problem
Each ticket tickets[i] = [from, to] is a one-way flight between two airports. A traveller starts at "JFK" and wants to use every ticket exactly once.
Return the itinerary as a list of airports in visiting order. If several itineraries use all the tickets, return the one that is smallest in lexical order when the airport codes are read as a sequence (compare the first airport that differs).
Examples
Example 1
- Input
tickets = [["SEA","DEN"],["JFK","ORD"],["ORD","SEA"],["DEN","BOS"]]- Output
["JFK","ORD","SEA","DEN","BOS"]- Why
- Only one order uses every ticket: the tickets simply chain together.
Example 2
- Input
tickets = [["JFK","MIA"],["JFK","ATL"],["ATL","JFK"],["MIA","ATL"]]- Output
["JFK","ATL","JFK","MIA","ATL"]- Why
- Both JFK -> MIA -> ATL -> JFK -> ATL and JFK -> ATL -> JFK -> MIA -> ATL use all tickets; the second is smaller in lexical order.
Example 3
- Input
tickets = [["JFK","AAA"],["JFK","BBB"],["BBB","JFK"]]- Output
["JFK","BBB","JFK","AAA"]- Why
- Flying to AAA first would strand you there with tickets left, so the itinerary must visit BBB first.
Constraints
1 <= tickets.length <= 5000; each ticket is[from, to]withfrom != to.- Every airport code is exactly 3 uppercase letters.
- The same ticket can appear more than once (each copy must be used).
- At least one itinerary starting at
"JFK"uses every ticket exactly once.
Solve it here
Write your solution in Java, Python, C++ or JavaScript and run it against the examples, right in the page. Submit runs the hidden tests too and, with a free account, records the solve in your revision log.
Your own testcases: Run checks these, and the expected output comes from our reference solution.
My submissions
Nothing yet. Every Run and Submit you make while signed in shows up here.
| When | Result | Tests | Runtime | Your time | Type |
|---|
Run checks the testcases above (3 examples, or your own). Submit also runs 11 hidden tests and saves the solve to your log. Signed in, every Run and Submit is saved under My submissions; your draft stays in this browser.
Solutions
Try it yourself first. Then compare: each approach lists its idea, the steps, its time and space, and the code in Java, Python, C++ and JavaScript.
| Approach | Time | Space |
|---|---|---|
| Brute force (backtracking) | O(E^E) worst case | O(E) |
| Optimal (Hierholzer's algorithm) | O(E log E) | O(E) |
1Brute force (backtracking)
O(E^E) worst caseA dead end can force the search to back up and retry many orders of the same tickets.O(E)The used flags, the route and the recursion stack are each at most E + 1 long.Sort the tickets by destination and try them in that order with DFS. Mark a ticket used, recurse, and undo it if the branch cannot use every ticket. The first complete route found is the lexically smallest one.
- Sort tickets by destination; group ticket indexes by their origin.
- dfs(airport): if the route has n + 1 airports, succeed.
- For each unused ticket out of airport, in destination order: mark it, append the destination, recurse; on failure unmark and remove.
- Start from JFK and return the route.
class Solution {
private List<String> route;
private Map<String, List<Integer>> out;
private List<List<String>> t;
private boolean[] used;
public List<String> findItinerary(List<List<String>> tickets) {
t = new ArrayList<>(tickets);
t.sort((a, b) -> a.get(1).compareTo(b.get(1)));
out = new HashMap<>();
for (int i = 0; i < t.size(); i++)
out.computeIfAbsent(t.get(i).get(0), x -> new ArrayList<>()).add(i);
used = new boolean[t.size()];
route = new ArrayList<>();
route.add("JFK");
dfs("JFK");
return route;
}
private boolean dfs(String at) {
if (route.size() == t.size() + 1) return true;
for (int i : out.getOrDefault(at, Collections.emptyList())) {
if (used[i]) continue;
used[i] = true;
route.add(t.get(i).get(1));
if (dfs(t.get(i).get(1))) return true;
route.remove(route.size() - 1);
used[i] = false;
}
return false;
}
}class Solution:
def findItinerary(self, tickets: List[List[str]]) -> List[str]:
t = sorted(tickets, key=lambda x: x[1])
out = {}
for i, (a, _) in enumerate(t):
out.setdefault(a, []).append(i)
used = [False] * len(t)
route = ["JFK"]
def dfs(at: str) -> bool:
if len(route) == len(t) + 1:
return True
for i in out.get(at, []):
if used[i]:
continue
used[i] = True
route.append(t[i][1])
if dfs(t[i][1]):
return True
route.pop()
used[i] = False
return False
dfs("JFK")
return routeclass Solution {
public:
vector<string> findItinerary(vector<vector<string>>& tickets) {
t = tickets;
stable_sort(t.begin(), t.end(), [](const vector<string>& a, const vector<string>& b) { return a[1] < b[1]; });
for (int i = 0; i < (int)t.size(); i++) out[t[i][0]].push_back(i);
used.assign(t.size(), false);
route = {"JFK"};
dfs("JFK");
return route;
}
private:
vector<vector<string>> t;
unordered_map<string, vector<int>> out;
vector<bool> used;
vector<string> route;
bool dfs(const string& at) {
if (route.size() == t.size() + 1) return true;
auto it = out.find(at);
if (it == out.end()) return false;
for (int i : it->second) {
if (used[i]) continue;
used[i] = true;
route.push_back(t[i][1]);
if (dfs(t[i][1])) return true;
route.pop_back();
used[i] = false;
}
return false;
}
};function findItinerary(tickets) {
const t = tickets.slice().sort((a, b) => (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0));
const out = new Map();
t.forEach(([a], i) => {
if (!out.has(a)) out.set(a, []);
out.get(a).push(i);
});
const used = new Array(t.length).fill(false);
const route = ["JFK"];
function dfs(at) {
if (route.length === t.length + 1) return true;
for (const i of out.get(at) || []) {
if (used[i]) continue;
used[i] = true;
route.push(t[i][1]);
if (dfs(t[i][1])) return true;
route.pop();
used[i] = false;
}
return false;
}
dfs("JFK");
return route;
}2Optimal (Hierholzer's algorithm)
O(E log E)Sorting the destination lists dominates; the walk itself pushes and pops each ticket once.O(E)The adjacency lists, the stack and the route all hold O(E) airports.Keep each airport's destinations sorted so the smallest one can be taken next. Walk greedily with an explicit stack: while the top airport has an unused ticket, take the smallest and push its destination. When the top has no tickets left, it is final in what remains of the trip, so pop it into the route. The route comes out backwards.
- For each origin, collect destinations and sort them in reverse so the smallest is at the end (cheap to remove).
- stack = [JFK].
- While the stack is not empty: if top has destinations, remove the last (smallest) and push it; else pop top and append it to route.
- Reverse route and return it.
class Solution {
public List<String> findItinerary(List<List<String>> tickets) {
Map<String, List<String>> out = new HashMap<>();
for (List<String> tk : tickets)
out.computeIfAbsent(tk.get(0), x -> new ArrayList<>()).add(tk.get(1));
for (List<String> l : out.values()) l.sort(Collections.reverseOrder());
Deque<String> stack = new ArrayDeque<>();
List<String> route = new ArrayList<>();
stack.push("JFK");
while (!stack.isEmpty()) {
List<String> next = out.get(stack.peek());
if (next != null && !next.isEmpty()) stack.push(next.remove(next.size() - 1));
else route.add(stack.pop());
}
Collections.reverse(route);
return route;
}
}class Solution:
def findItinerary(self, tickets: List[List[str]]) -> List[str]:
out = {}
for a, b in tickets:
out.setdefault(a, []).append(b)
for lst in out.values():
lst.sort(reverse=True)
stack = ["JFK"]
route = []
while stack:
nxt = out.get(stack[-1])
if nxt:
stack.append(nxt.pop())
else:
route.append(stack.pop())
return route[::-1]class Solution {
public:
vector<string> findItinerary(vector<vector<string>>& tickets) {
unordered_map<string, vector<string>> out;
for (auto& tk : tickets) out[tk[0]].push_back(tk[1]);
for (auto& p : out) sort(p.second.rbegin(), p.second.rend());
vector<string> stack = {"JFK"}, route;
while (!stack.empty()) {
auto it = out.find(stack.back());
if (it != out.end() && !it->second.empty()) {
string nxt = it->second.back();
it->second.pop_back();
stack.push_back(nxt);
} else {
route.push_back(stack.back());
stack.pop_back();
}
}
reverse(route.begin(), route.end());
return route;
}
};function findItinerary(tickets) {
const out = new Map();
for (const [a, b] of tickets) {
if (!out.has(a)) out.set(a, []);
out.get(a).push(b);
}
for (const lst of out.values()) lst.sort().reverse();
const stack = ["JFK"];
const route = [];
while (stack.length > 0) {
const nxt = out.get(stack[stack.length - 1]);
if (nxt && nxt.length > 0) stack.push(nxt.pop());
else route.push(stack.pop());
}
return route.reverse();
}Edge cases to test
- A single ticket
- Taking the lexically smallest destination leads to a dead end
- Duplicate tickets between the same two airports
- The trip passes through JFK several times
Hints
Hint 1
This is a path that uses every edge exactly once: an Eulerian path in a directed graph.
Hint 2
Greedily taking the smallest destination can get stuck, but the stuck airport must be the very end of the trip.
Hint 3
Hierholzer: follow the smallest unused ticket until you cannot move, then add the airport to the route on the way back. Reverse at the end.
FAQ
What is the best time complexity for Reconstruct Itinerary?
Optimal (Hierholzer's algorithm) runs in O(E log E) time and O(E) extra space. Sorting the destination lists dominates; the walk itself pushes and pops each ticket once.
Which pattern does Reconstruct Itinerary use?
It is a graphs problem that uses the eulerian path (hierholzer) pattern.
Is there a brute force solution for Reconstruct Itinerary?
Yes. Brute force (backtracking) takes O(E^E) worst case time and O(E) space. Sort the tickets by destination and try them in that order with DFS.
Which edge cases should I test for Reconstruct Itinerary?
A single ticket; Taking the lexically smallest destination leads to a dead end; Duplicate tickets between the same two airports; The trip passes through JFK several times.