DFS intuition#
First we must build a graph where airports are vertices and an edge airport1 -> airport2
denotes that the person travelled from airport1 -> airport2
To find a valid sequence of vertices/airports
that covers all the edges/tickets
, we traverse the graph using depth first search like traversal. The idea is:
- At each vertex, move to the next lexicographically smallest vertex.
- delete the edge used
To maintain lexicographical order, we sort the neighbours of each vertex lexicographically.
Algorithm:
Step 1: Build an adjacency list, where the neighbours of each node are sorted lexicographically
Step 2: Traverse using DFS starting from “JFK” airport. Remove edges as you move through the graph.
Incorrect approach#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| /**
* @param {string[][]} tickets
* @return {string[]}
*/
var findItinerary = function(tickets) {
const graph = {};
for ( const [from, to] of tickets ) {
if ( !graph[from] ) graph[from] = [];
graph[from].push(to);
}
for ( const node of Object.keys(graph) )
graph[node].sort();
const path = [];
const dfs = ( node = "JFK" ) => {
path.push(node);
const to_where = graph[node] || [];
while ( to_where.length ) {
// remove the edge and visit the next smallest
dfs( to_where.shift() );
}
};
dfs();
return path;
};
|
Why it does not work:#
Consider this example: [[“JFK”,“KUL”],[“JFK”,“NRT”],[“NRT”,“JFK”]]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
| +---+ +---+
|JFK|---->|KUL|
+---+ +---+
| ^
| |
v |
+---+
|NRT|
+---+
path = []
# algorithm starts at JFK
@JFK -> add(JFK)
neighbours -> [KUL, NRT]
explore(KUL) and remove edge JFK->KUL ----(call 1)
graph becomes: path = [JFK]
+---+ +---+
|JFK| |KUL|
+---+ +---+
| ^
| |
v |
+---+
|NRT|
+---+
@KUL -> add(KUL)
there are no neighbours so return to call(1)
graph becomes: path = [JFK, KUL]
+---+ +---+
|JFK| |KUL|
+---+ +---+
| ^
| |
v |
+---+
|NRT|
+---+
# So you see, we are again at JFK, even though there
# is no edge from KUL to JFK
@JFK again
neighbours now -> [NRT]
explore(NRT) and remove edge JFK->NRT ----(call 2)
+---+ +---+
|JFK| |KUL|
+---+ +---+
^
|
|
+---+
|NRT|
+---+
@NRT -> add(NRT)
neighbours -> [JFK]
explore(JFK) and delete NRT -> JFK --- call(3)
graph becomes:
+---+ +---+ path = [JFK, KUL, NRT]
|JFK| |KUL|
+---+ +---+
+---+
|NRT|
+---+
@ JFK -> add(JFK)
no neighnour so return to call(3)
graph becomes:
+---+ +---+ path = [JFK, KUL, NRT, JFK]
|JFK| |KUL|
+---+ +---+
+---+
|NRT|
+---+
@NRT (call 3) -> no more neighbours so return to call 2
@JFK (call 2) -> no more neighbours so algorithm ends
|
The final path is: [JFK,KUL,NRT,JFK]
If we look at our tickets again: [[JFK, KUL],[JFK, NRT],[NRT, JFK]]
We go from JFK
to KUL
using the ticket [JFK, KUL]
. Then we move from KUL
to NRT
even though there is no direct ticket [KUL, NRT]
.
From our dry run, we saw how the algorithm transported us back from KUL
to JFK
, by magically teleporting us.
Correct approach#
So how to fix the problem above. The fix is simple, we shall only add a vertex, if all subgraph containing that vertex has been fully explored. This approach ensures that we are only using continous links.
This code is just 2 lines away from the dfs code above. I added comments on what changed and why.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| /**
* @param {string[][]} tickets
* @return {string[]}
*/
var findItinerary = function(tickets) {
const graph = {};
for ( const [from, to] of tickets ) {
if ( !graph[from] ) graph[from] = [];
graph[from].push(to);
}
for ( const node of Object.keys(graph) )
graph[node].sort();
const path = [];
const dfs = ( node = "JFK" ) => {
const to_where = graph[node] || [];
while ( to_where.length )
dfs( to_where.shift() );
path.push(node); // add node only after all
// the subgraph has been explored
};
dfs();
// we changed the order of insertion, and
// we must reverse the path to obtain the original
// itinerary
return path.reverse();
};
|
Complexity#
- Time: $$O( E )$$
- Space: $$ O( V*E ) $$ // for storing the graph
Additional info:#
An Eulerian trail or Euler walk, in an undirected graph is a walk that uses each edge exactly once. There are mainly 2 algorithms to solve this problem: Fleury’s algorithm, Hierholzer’s algorithm. [source: https://en.wikipedia.org/wiki/Eulerian_path]
What we did above is a varient of Heirholzer’s algorithm.