Given a list of airline tickets, reconstruct the itinerary starting from JFK that uses every ticket exactly once. If multiple valid orderings exist, return the one that is lexicographically smallest. Hierholzer's algorithm — DFS with post-order append and a final reverse — finds the Eulerian path in one linear pass.
Given a list of [from, to] airline tickets, find an itinerary that starts at JFK, uses every ticket exactly once, and is lexicographically smallest if multiple valid orderings exist.
Concrete example:
Tickets: [["JFK","MUC"],["MUC","LHR"],["LHR","SFO"],["SFO","SJC"]]
Expected output: ["JFK","MUC","LHR","SFO","SJC"]
There is only one valid Eulerian path here — follow the unique chain JFK → MUC → LHR → SFO → SJC.
[from, to] ticket, append to to adj[from]. Use a Map<string, string[]> so each airport stores its outbound destinations.dests.sort() on each list so we always try the lexicographically smallest neighbor first — this guarantees the smallest valid itinerary.shift() (remove) the smallest destination and recurse into it. Using shift() marks the edge as used so we never take it twice.while loop empties, push the current airport onto route. Airports with no remaining outgoing edges (dead-ends or the true tail) are appended first.route.reverse() produces the correct forward order.Space is O(E) for the adjacency lists and the recursion stack depth (at most E frames). The sort is the only non-linear cost; if the destinations were already sorted (or you used a min-heap), the DFS itself runs in O(E).
JFK.1function findItinerary(tickets: string[][]): string[] {2 // Build adjacency list: sorted in ascending (lexicographic) order per source3▶ const adj = new Map<string, string[]>();4▶ for (const [from, to] of tickets) {5▶ if (!adj.has(from)) adj.set(from, []);6▶ adj.get(from)!.push(to);7▶ }8▶ for (const dests of adj.values()) {9▶ dests.sort(); // visit smallest destination first10▶ }1112 const route: string[] = [];1314 function dfs(airport: string): void {15 const dests = adj.get(airport);16 while (dests && dests.length > 0) {17 const next = dests.shift()!; // consume smallest destination18 dfs(next);19 }20 route.push(airport); // post-order: push AFTER exhausting all departures21 }2223 dfs('JFK');24 return route.reverse(); // reverse gives forward itinerary25}
function findItinerary(tickets: string[][]): string[] {
// Build adjacency list: sorted in ascending (lexicographic) order per source
const adj = new Map<string, string[]>();
for (const [from, to] of tickets) {
if (!adj.has(from)) adj.set(from, []);
adj.get(from)!.push(to);
}
for (const dests of adj.values()) {
dests.sort(); // visit smallest destination first
}
const route: string[] = [];
function dfs(airport: string): void {
const dests = adj.get(airport);
while (dests && dests.length > 0) {
const next = dests.shift()!; // consume smallest destination
dfs(next);
}
route.push(airport); // post-order: push AFTER exhausting all departures
}
dfs('JFK');
return route.reverse(); // reverse gives forward itinerary
}adj[from]. The guard if (!adj.has(from)) adj.set(from, []) initializes the array on first encounter.dests.sort() on each adjacency list guarantees we always visit the lexicographically smallest neighbor first, which is what produces the smallest valid itinerary without any backtracking.while loop dests.shift()s the smallest remaining destination and recurses. Using shift() removes the edge permanently so it cannot be taken twice.route.push(airport) records this airport. Because dead-ends are exhausted first, they appear earliest in route — which is why we need the reverse."JFK" as guaranteed by the problem. After DFS completes, route.reverse() flips the post-order accumulation into the correct forward itinerary.JFK→A appearing twice simply means the JFK list contains two A entries, both consumed separately.function findItinerary(tickets: string[][]): string[] {
// Build adjacency list: sorted in ascending (lexicographic) order per source
const adj = new Map<string, string[]>();
for (const [from, to] of tickets) {
if (!adj.has(from)) adj.set(from, []);
adj.get(from)!.push(to);
}
for (const dests of adj.values()) {
dests.sort(); // visit smallest destination first
}
const route: string[] = [];
function dfs(airport: string): void {
const dests = adj.get(airport);
while (dests && dests.length > 0) {
const next = dests.shift()!; // consume smallest destination
dfs(next);
}
route.push(airport); // post-order: push AFTER exhausting all departures
}
dfs('JFK');
return route.reverse(); // reverse gives forward itinerary
}Try each unused ticket in lexicographic order and backtrack whenever a branch cannot consume all tickets; the firstitinerary that uses every ticket is automatically the smallest, since we always explore smaller destinations first. It is slower than Hierholzer's because failed branches are unwound and retried instead of being threaded into one linear post-order walk.
function findItinerary(tickets: string[][]): string[] {
// Build adjacency: each origin -> targets sorted lexicographically.
const adj = new Map<string, string[]>();
for (const [from, to] of tickets) {
const list = adj.get(from);
if (list) list.push(to);
else adj.set(from, [to]);
}
for (const dests of adj.values()) {
dests.sort();
}
// Track which tickets are still available via a "used" flag per slot.
const used = new Map<string, boolean[]>();
for (const [airport, dests] of adj) {
used.set(airport, new Array<boolean>(dests.length).fill(false));
}
const totalTickets = tickets.length;
const route: string[] = ['JFK'];
function backtrack(airport: string): boolean {
// A complete itinerary visits every ticket: route length = edges + 1.
if (route.length === totalTickets + 1) return true;
const dests = adj.get(airport);
if (!dests) return false;
const usedFlags = used.get(airport)!;
for (let i = 0; i < dests.length; i++) {
if (usedFlags[i]) continue;
// Skip exploring an identical destination already tried at this depth,
// so duplicate edges don't redo identical work.
if (i > 0 && dests[i] === dests[i - 1] && !usedFlags[i - 1]) continue;
usedFlags[i] = true;
route.push(dests[i]);
if (backtrack(dests[i])) return true; // first success is lexicographically smallest
// Branch failed to use all tickets: undo and try the next target.
route.pop();
usedFlags[i] = false;
}
return false;
}
backtrack('JFK');
return route;
}dests[i] === dests[i - 1]) is a small pruning that avoids re-exploring identical edges; remove it and the result is still correct, just slower on inputs with repeated tickets. Because targets are pre-sorted and we return on the first complete path, no explicit comparison of candidate itineraries is ever needed.| "use every ticket / edge exactly once" | Hierholzer Eulerian path DFS |
| lexicographically smallest valid ordering | pre-sort adjacency lists |
| greedy DFS hits dead-end before all edges used | post-order push + reverse |
| directed graph, all edges must be traversed | Eulerian path existence check |
const adj = new Map<string, string[]>();
for (const [from, to] of tickets) {
if (!adj.has(from)) adj.set(from, []);
adj.get(from)!.push(to);
}
for (const dests of adj.values()) dests.sort();
const route: string[] = [];
function dfs(airport: string): void {
const dests = adj.get(airport);
while (dests && dests.length > 0) {
dfs(dests.shift()!);
}
route.push(airport); // post-order push
}
dfs('JFK');
return route.reverse();route.push(airport) after the while loop, not before?[["JFK","A"],["A","B"],["JFK","B"],["B","JFK"]]. What is the output?