Skip to content

Commit 0d9a4ce

Browse files
committed
feat(algorithms, graphs): reconstruct itinerary
1 parent 9b8a6d4 commit 0d9a4ce

21 files changed

+412
-0
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Reconstruct Itinerary
2+
3+
Given a list of airline tickets where tickets[i] = [fromi, toi] represent a departure airport and an arrival airport of
4+
a single flight, reconstruct the itinerary in the correct order and return it.
5+
6+
The person who owns these tickets always starts their journey from "JFK". Therefore, the itinerary must begin with "JFK".
7+
If there are multiple valid itineraries, you should prioritize the one with the smallest lexical order when considering
8+
a single string.
9+
10+
> Lexicographical order is a way of sorting similar to how words are arranged in a dictionary. It compares items
11+
> character by character, based on their order in the alphabet or numerical value.
12+
13+
- For example, the itinerary ["JFK", "EDU"] has a smaller lexical order than ["JFK", "EDX"].
14+
15+
> Note: You may assume all tickets form at least one valid itinerary. You must use all the tickets exactly once.
16+
17+
## Constraints
18+
19+
- 1 <= `tickets.length` <= 300
20+
- `tickets[i].length` == 2
21+
- `fromi.length` == 3
22+
- `toi.length` == 3
23+
- `fromi` != `toi`
24+
- `fromi` and `toi` consist of upper case English letters
25+
26+
## Examples
27+
28+
![Example 1](./images/examples/reconstruct_itinerary_example_1.png)
29+
![Example 2](./images/examples/reconstruct_itinerary_example_2.png)
30+
![Example 3](./images/examples/reconstruct_itinerary_example_3.png)
31+
32+
## Related Topics
33+
34+
- Depth First Search
35+
- Graph
36+
- Eulerian Circuit
37+
38+
## Solution
39+
40+
The algorithm uses __Hierholzer’s algorithm__ to reconstruct travel itineraries from a list of airline tickets. This
41+
problem is like finding an __Eulerian path__ but with a fixed starting point, “JFK”. Hierholzer’s algorithm is great for
42+
finding Eulerian paths and cycles, which is why we use it here.
43+
44+
> Hierholzer's algorithm is a method for finding an Eulerian circuit (a cycle that visits every edge exactly once) in a
45+
> graph. It starts from any vertex and follows edges until it returns to the starting vertex, forming a cycle. If there
46+
> are any unvisited edges, it starts a new cycle from a vertex on the existing cycle that has unvisited edges and merges
47+
> the cycles. The process continues until all edges are visited.
48+
49+
> An Eulerian path is a trail in a graph that visits every edge exactly once. An Eulerian path can exist only if exactly
50+
> zero or two vertices have an odd degree. If there are exactly zero vertices with an odd degree, the path can form a
51+
> circuit (Eulerian circuit), where the starting and ending points are the same. If there are exactly two vertices with
52+
> an odd degree, the path starts at one of these vertices and ends at the other.
53+
54+
The algorithm starts by arranging the destinations in reverse lexicographical order to ensure we always choose the
55+
smallest destination first. It then uses depth-first search (DFS) starting from “JFK” to navigate the flights. As it
56+
explores each flight path, it builds the itinerary by appending each visited airport when there are no more destinations
57+
to visit from that airport. Since the airports are added in reverse order during this process, the final step is to
58+
reverse the list to get the correct itinerary.
59+
60+
The basic algorithm to solve this problem will be:
61+
62+
1. Create a dictionary, `flight_map`, to store the flight information. Each key represents an airport; its corresponding
63+
value is a list of destinations from that airport.
64+
2. Initialize an empty list, result, to store the reconstructed itinerary.
65+
3. Sort the destinations lexicographically in reverse order to ensure that the smallest destination is chosen first.
66+
4. Perform DFS traversal starting from the airport "JFK".
67+
- Get the list of destinations for the current airport from flight_map.
68+
- While there are destinations available:
69+
- Pop the next_destination from destinations.
70+
- Recursively explore all available flights starting from the popped next_destination, until all possible flights
71+
have been considered.
72+
- Append the current airport to the result list.
73+
5. Return the result list in reverse order to ensure the itinerary starts from the initial airport, "JFK", and proceeds
74+
through the subsequent airports in the correct order.
75+
76+
Let’s look at the following illustration to get a better understanding of the solution:
77+
78+
![Solution 1](./images/solutions/reconstruct_itinerary_solution_1.png)
79+
![Solution 2](./images/solutions/reconstruct_itinerary_solution_2.png)
80+
![Solution 3](./images/solutions/reconstruct_itinerary_solution_3.png)
81+
![Solution 4](./images/solutions/reconstruct_itinerary_solution_4.png)
82+
![Solution 5](./images/solutions/reconstruct_itinerary_solution_5.png)
83+
![Solution 6](./images/solutions/reconstruct_itinerary_solution_6.png)
84+
![Solution 7](./images/solutions/reconstruct_itinerary_solution_7.png)
85+
![Solution 8](./images/solutions/reconstruct_itinerary_solution_8.png)
86+
![Solution 9](./images/solutions/reconstruct_itinerary_solution_9.png)
87+
![Solution 10](./images/solutions/reconstruct_itinerary_solution_10.png)
88+
![Solution 11](./images/solutions/reconstruct_itinerary_solution_11.png)
89+
![Solution 12](./images/solutions/reconstruct_itinerary_solution_12.png)
90+
![Solution 13](./images/solutions/reconstruct_itinerary_solution_13.png)
91+
![Solution 14](./images/solutions/reconstruct_itinerary_solution_14.png)
92+
![Solution 15](./images/solutions/reconstruct_itinerary_solution_15.png)
93+
94+
### Time Complexity
95+
96+
Each edge (flight) is traversed once during the DFS process in the algorithm, resulting in a complexity proportional to
97+
the number of edges, ∣E∣.
98+
Before DFS, the outgoing edges for each airport must be sorted. The sorting operation’s complexity depends on the input
99+
graph’s structure.
100+
In the worst-case scenario, such as a highly unbalanced graph (e.g., star-shaped), where one airport (e.g., JFK)
101+
dominates the majority of flights, the sorting operation on this airport becomes highly expensive, possibly reaching N log N
102+
complexity where `N = |E|/2` In a more balanced or average scenario, where each airport has a roughly equal number of
103+
outgoing flights, the sorting operation complexity remains O(N log N) where N represents half of the total number of
104+
edges divided by twice the number of airports O(|E|/2|V|). Thus, the algorithm’s overall complexity is O(|E|log|E/2|),
105+
emphasizing the significance of the sorting operation in determining its performance.
106+
107+
### Space Complexity
108+
109+
The space complexity is O(∣V∣+∣E∣), where ∣V∣ is the number of airports and ∣E∣ is the number of flights.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from typing import List, DefaultDict
2+
from collections import defaultdict
3+
4+
5+
def find_itinerary(tickets: List[List[str]]) -> List[str]:
6+
"""
7+
Reconstructs the itinerary from a given list of lists of tickets.
8+
Args:
9+
tickets (list): list of lists of tickets
10+
Returns:
11+
List[str]: reconstructed tickets
12+
"""
13+
if len(tickets) == 0:
14+
return []
15+
16+
graph = {}
17+
# Create a graph for each airport and keep list of airport reachable from it
18+
for src, dst in tickets:
19+
if src in graph:
20+
graph[src].append(dst)
21+
else:
22+
graph[src] = [dst]
23+
24+
for src in graph.keys():
25+
graph[src].sort(reverse=True)
26+
# Sort children list in descending order so that we can pop last element
27+
# instead of pop out first element which is costly operation
28+
stack = []
29+
res = []
30+
stack.append("JFK")
31+
# Start with JFK as starting airport and keep adding the next child to traverse
32+
# for the last airport at the top of the stack. If we reach to an airport from where
33+
# we can't go further then add it to the result. This airport should be the last to go
34+
# since we can't go anywhere from here. That's why we return the reverse of the result
35+
# After this backtrack to the top airport in the stack and continue to traaverse it's children
36+
37+
while len(stack) > 0:
38+
elem = stack[-1]
39+
if elem in graph and len(graph[elem]) > 0:
40+
# Check if elem in graph as there may be a case when there is no out edge from an airport
41+
# In that case it won't be present as a key in graph
42+
stack.append(graph[elem].pop())
43+
else:
44+
res.append(stack.pop())
45+
# If there is no further children to traverse then add that airport to res
46+
# This airport should be the last to go since we can't anywhere from this
47+
# That's why we return the reverse of the result
48+
return res[::-1]
49+
50+
51+
def find_itinerary_using_hierholzers(tickets: List[List[str]]) -> List[str]:
52+
flight_map: DefaultDict[str, List[str]] = defaultdict(list)
53+
result: List[str] = []
54+
55+
# Populate the flight map with each departure and arrival
56+
for departure, arrival in tickets:
57+
flight_map[departure].append(arrival)
58+
59+
# Sort each list of destinations in reverse lexicographical order
60+
for departure in flight_map:
61+
flight_map[departure].sort(reverse=True)
62+
63+
def dfs_traversal(
64+
current: str, flights: DefaultDict[str, List[str]], res: List[str]
65+
):
66+
destinations = flights[current]
67+
68+
# Traverse all destinations in the order of their lexicographical sorting
69+
while destinations:
70+
# Pop the last destination from the list (smallest lexicographical order due to reverse sorting)
71+
next_destination = destinations.pop()
72+
# Recursively perform DFS on the next destination
73+
dfs_traversal(next_destination, flights, res)
74+
75+
# Append the current airport to the result after all destinations are visited
76+
res.append(current)
77+
78+
dfs_traversal("JFK", flight_map, result)
79+
80+
return result[::-1]
64.6 KB
Loading
70.2 KB
Loading
70 KB
Loading
69.3 KB
Loading
95.1 KB
Loading
98.6 KB
Loading
98.3 KB
Loading
92.2 KB
Loading

0 commit comments

Comments
 (0)