1
1
2
- ## 题目地址
3
- https://leetcode-cn.com/problems/reconstruct-itinerary/
2
+ > 这也可以用回溯法? 其实深搜和回溯也是相辅相成的,毕竟都用递归。
4
3
5
- # 332. 重新安排行程
4
+ # 332.重新安排行程
5
+
6
+ 题目地址:https://leetcode-cn.com/problems/reconstruct-itinerary/
7
+
8
+ 给定一个机票的字符串二维数组 [ from, to] ,子数组中的两个成员分别表示飞机出发和降落的机场地点,对该行程进行重新规划排序。所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。
9
+
10
+ 提示:
11
+ * 如果存在多种有效的行程,请你按字符自然排序返回最小的行程组合。例如,行程 [ "JFK", "LGA"] 与 [ "JFK", "LGB"] 相比就更小,排序更靠前
12
+ * 所有的机场都用三个大写字母表示(机场代码)。
13
+ * 假定所有机票至少存在一种合理的行程。
14
+ * 所有的机票必须都用一次 且 只能用一次。
15
+
16
+
17
+ 示例 1:
18
+ 输入:[[ "MUC", "LHR"] , [ "JFK", "MUC"] , [ "SFO", "SJC"] , [ "LHR", "SFO"]]
19
+ 输出:[ "JFK", "MUC", "LHR", "SFO", "SJC"]
20
+
21
+ 示例 2:
22
+ 输入:[[ "JFK","SFO"] ,[ "JFK","ATL"] ,[ "SFO","ATL"] ,[ "ATL","JFK"] ,[ "ATL","SFO"]]
23
+ 输出:[ "JFK","ATL","JFK","SFO","ATL","SFO"]
24
+ 解释:另一种有效的行程是 [ "JFK","SFO","ATL","JFK","ATL","SFO"] 。但是它自然排序更大更靠后。
6
25
7
26
# 思路
8
27
9
- 举一个有重复机场的例子:
28
+ 这道题目还是很难的,之前我们用回溯法解决了如下问题:[ 组合问题] ( https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ ) ,[ 分割问题] ( https://mp.weixin.qq.com/s/v--VmA8tp9vs4bXCqHhBuA ) ,[ 子集问题] ( https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA ) ,[ 排列问题] ( https://mp.weixin.qq.com/s/SCOjeMX1t41wcvJq49GhMw ) 。
29
+
30
+ 直觉上来看 这道题和回溯法没有什么关系,更像是图论中的深度优先搜索。
31
+
32
+ 实际上确实是深搜,但这是深搜中使用了回溯的例子,在查找路径的时候,如果不回溯,怎么能查到目标路径呢。
33
+
34
+ 所以我倾向于说本题应该使用回溯法,那么我也用回溯法的思路来讲解本题,其实深搜一般都使用了回溯法的思路,在图论系列中我会再详细讲解深搜。
10
35
11
- <img src =' ../pics/332.重新安排行程.png ' width =600 > </img ></div >
36
+ ** 这里就是先给大家拓展一下,原来回溯法还可以这么玩!**
37
+
38
+ ** 这道题目有几个难点:**
39
+
40
+ 1 . 一个行程中,如果航班处理不好容易变成一个圈,成为死循环
41
+ 2 . 有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ?
42
+ 3 . 使用回溯法(也可以说深搜) 的话,那么终止条件是什么呢?
43
+ 4 . 搜索的过程中,如何遍历一个机场所对应的所有机场。
44
+
45
+ 针对以上问题我来逐一解答!
46
+
47
+ ## 如何理解死循环
48
+
49
+ 对于死循环,我来举一个有重复机场的例子:
50
+
51
+ ![ 332.重新安排行程] ( https://img-blog.csdnimg.cn/20201115180537865.png )
12
52
13
53
为什么要举这个例子呢,就是告诉大家,出发机场和到达机场也会重复的,** 如果在解题的过程中没有对集合元素处理好,就会死循环。**
14
54
15
- 这道题目有几个难点:
55
+ ## 该记录映射关系
56
+
57
+ 有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ?
58
+
59
+ 一个机场映射多个机场,机场之间要靠字母序排列,一个机场映射多个机场,可以使用std::unordered_map,如果让多个机场之间再有顺序的话,就是用std::map 或者std::multimap 或者 std::multiset。
16
60
17
- 1 . 有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ?
18
- 2 . 这是一个图不是一棵树,使用深搜/回溯 终止条件是什么呢?
19
- 3 . 回溯的过程中,如何遍历一个城市所对应的所有城市。
61
+ 如果对map 和 set 的实现机制不太了解,也不清楚为什么 map、multimap就是有序的同学,可以看这篇文章[ 关于哈希表,你该了解这些!] ( https://mp.weixin.qq.com/s/g8N6WmoQmsCUw3_BaWxHZA ) 。
62
+
63
+ 这样存放映射关系可以定义为 ` unordered_map<string, multiset<string>> targets ` 或者 ` unordered_map<string, map<string, int>> targets ` 。
64
+
65
+ 含义如下:
66
+
67
+ ` unordered_map<string, multiset<string>> targets ` :` unordered_map<出发机场, 到达机场的集合> targets `
68
+ ` unordered_map<string, map<string, int>> targets ` :` unordered_map<出发机场, map<到达机场, 航班次数>> targets `
69
+
70
+ 这两个结构,我选择了后者,因为如果使用` unordered_map<string, multiset<string>> targets ` 遍历multiset的时候,不能删除元素,一旦删除元素,迭代器就失效了。
71
+
72
+ ** 再说一下为什么一定要增删元素呢,正如开篇我给出的图中所示,出发机场和到达机场是会重复的,搜索的过程没及时删除目的机场就会死循环。**
20
73
21
- 首先这道题目是使用回溯法(也可以说深搜),那么按照我总结的回溯模板来。
74
+ 所以搜索的过程中就是要不断的删multiset里的元素,那么推荐使用` unordered_map<string, map<string, int>> targets ` 。
75
+
76
+ 在遍历 ` unordered_map<出发机场, map<到达机场, 航班次数>> targets ` 的过程中,** 可以使用"航班次数"这个字段的数字做相应的增减,来标记到达机场是否使用过了。**
77
+
78
+
79
+ 如果“航班次数”大于零,说明目的地还可以飞,如果如果“航班次数”等于零说明目的地不能飞了,而不用对集合做删除元素或者增加元素的操作。
80
+
81
+ ** 相当于说我不删,我就做一个标记!**
82
+
83
+ ## 回溯法
84
+
85
+ 这道题目我使用回溯法,那么下面按照我总结的回溯模板来:
22
86
23
87
```
24
- backtracking() {
88
+ void backtracking(参数 ) {
25
89
if (终止条件) {
26
90
存放结果;
91
+ return;
27
92
}
28
93
29
- for (枚举同一个位置的所有可能性,可以想成节点孩子的数量 ) {
30
- 递归, 处理节点;
31
- backtracking();
94
+ for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小) ) {
95
+ 处理节点;
96
+ backtracking(路径,选择列表); // 递归
32
97
回溯,撤销处理结果
33
98
}
34
99
}
35
100
```
36
101
37
- ## 1. 有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ?
102
+ 本题以输入: [[ "JFK", "KUL" ] , [ "JFK", "NRT" ] , [ "NRT", "JFK" ] 为例,抽象为树形结构如下:
38
103
39
- 一个城市映射多个城市,城市之间要靠字母序排列,一个城市映射多个城市,可以使用std::unordered_map,如果让多个城市之间再有顺序的话,就是用std::map 或者 或者std::multimap 或者 std::multiset。
104
+ < img src = ' https://img-blog.csdnimg.cn/2020111518065555.png ' width = 600 > </ img ></ div >
40
105
41
- 如果对map 和 set 的实现机制不太了解,也不清楚为什么 map、multimap就是有序的同学,可以看这篇文章 [ 关于哈希表,你该了解这些! ] ( https://mp.weixin.qq.com/s/g8N6WmoQmsCUw3_BaWxHZA ) 。
106
+ 开始回溯三部曲讲解:
42
107
43
- 这样存放映射关系可以定义为 ` unordered_map<string, multiset<string>> targets ` 或者 ` unordered_map<string, map<string, int>> targets ` 。
108
+ * 递归函数参数
44
109
45
- 含义如下:
110
+ 在讲解映射关系的时候,已经讲过了,使用` unordered_map<string, map<string, int>> targets; ` 来记录航班的映射关系,我定义为全局变量。
111
+
112
+ 当然把参数放进函数里传进去也是可以的,我是尽量控制函数里参数的长度。
113
+
114
+ 参数里还需要ticketNum,表示有多少个航班(终止条件会用上)。
115
+
116
+ 代码如下:
117
+
118
+ ```
119
+ // unordered_map<出发机场, map<到达机场, 航班次数>> targets
120
+ unordered_map<string, map<string, int>> targets;
121
+ bool backtracking(int ticketNum, vector<string>& result) {
122
+ ```
123
+
124
+ ** 注意函数返回值我用的是bool!**
46
125
47
- ` unordered_map<string, multiset<string>> targets ` :` unordered_map<出发城市, 到达城市的集合> targets `
48
- ` unordered_map<string, map<string, int>> targets ` :` unordered_map<出发城市, map<到达城市, 航班次数>> targets `
126
+ 我们之前讲解回溯算法的时候,一般函数返回值都是void,这次为什么是bool呢?
49
127
50
- 这两个结构,我们选择了后者,因为如果使用 ` unordered_map<string, multiset<string>> targets ` 遍历multiset的时候,不能删除元素,一旦删除元素,迭代器就失效了。而本地在回溯的过程中就是要不断的增删 multiset里的元素,所以 我们使用 ` unordered_map<string, map<string, int>> targets ` 。
128
+ 因为我们只需要找到一个行程,就是在树形结构中唯一的一条通向叶子节点的路线,如图:
51
129
52
- 在遍历 ` unordered_map<出发城市, map<到达城市, 航班次数>> targets ` 的过程中,可以使用航班次数这个字段的数字 --或者++,来标记到达城市是否使用过了,而不用对集合做删除元素或者增加元素的操作。
130
+ < img src = ' https://img-blog.csdnimg.cn/2020111518065555.png ' width = 600 > </ img ></ div >
53
131
54
- ## 2. 这是一个图不是一棵树,使用深搜/回溯 终止条件是什么呢?
132
+ 所以找到了这个叶子节点了直接返回,这个递归函数的返回值问题我们在讲解二叉树的系列的时候,在这篇 [ 二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值? ] ( https://mp.weixin.qq.com/s/6TWAVjxQ34kVqROWgcRFOg ) 详细介绍过。
55
133
56
- 你看有多少个航班,那题目中的示例为例,输入: [[ "MUC", "LHR"] , [ "JFK", "MUC"] , [ "SFO", "SJC"] , [ "LHR", "SFO"]] ,这是有4个航班,那么只要找出一种行程,行程里的机场个数是5就可以了。
134
+ 当然本题的targets和result都需要初始化,代码如下:
135
+ ```
136
+ for (const vector<string>& vec : tickets) {
137
+ targets[vec[0]][vec[1]]++; // 记录映射关系
138
+ }
139
+ result.push_back("JFK"); // 起始机场
140
+ ```
57
141
58
- 所以终止条件 我们回溯遍历的过程中,遇到的机场个数,如果达到了(航班数量+1),那么我们就找到了一个行程,把所有航班串在一起了。
142
+ * 递归终止条件
59
143
60
- ## 3. 回溯的过程中,如何遍历一个城市所对应的所有城市 。
144
+ 拿题目中的示例为例,输入: [[ "MUC", "LHR" ] , [ "JFK", "MUC" ] , [ "SFO", "SJC" ] , [ "LHR", "SFO" ]] ,这是有4个航班,那么只要找出一种行程,行程里的机场个数是5就可以了 。
61
145
62
- 这里刚刚说过,在选择映射函数的时候,不能选择 ` unordered_map<string, multiset<string>> targets ` , 因为一旦有元素增删multiset的迭代器就会失效,有一些题解使用了 例如list的迭代器,使用splice来保证 迭代器不失效。
146
+ 所以终止条件是:我们回溯遍历的过程中,遇到的机场个数,如果达到了(航班数量+1),那么我们就找到了一个行程,把所有航班串在一起了。
63
147
64
- 可以说既要找到一个对数据经行排序的容器,而且这个容易增删元素,迭代器还不能失效。
148
+ 代码如下:
65
149
66
- ** 再说一下为什么一定要增删元素呢,正如开篇我给出的图中所示,出发机场和到达机场是会重复的,搜索的过程没及时删除元素就会死循环。**
150
+ ```
151
+ if (result.size() == ticketNum + 1) {
152
+ return true;
153
+ }
154
+ ```
67
155
68
- 所以我选择了` unordered_map<string, map<string, int>> targets ` 来基于映射条件。
156
+ 已经看习惯回溯法代码的同学,到叶子节点了习惯性的想要收集结果,但发现并不需要,本题的result相当于 [ 回溯算法:求组合总和!] ( https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w ) 中的path,也就是本题的result就是记录路径的(就一条),在如下单层搜索的逻辑中result就添加元素了。
157
+
158
+ * 单层搜索的逻辑
159
+
160
+ 回溯的过程中,如何遍历一个机场所对应的所有机场呢?
161
+
162
+ 这里刚刚说过,在选择映射函数的时候,不能选择` unordered_map<string, multiset<string>> targets ` , 因为一旦有元素增删multiset的迭代器就会失效,当然可能有牛逼的容器删除元素迭代器不会失效,这里就不在讨论了。
163
+
164
+ ** 可以说本题既要找到一个对数据进行排序的容器,而且还要容易增删元素,迭代器还不能失效** 。
165
+
166
+ 所以我选择了` unordered_map<string, map<string, int>> targets ` 来做机场之间的映射。
69
167
70
168
遍历过程如下:
71
169
72
170
```
73
171
for (pair<const string, int>& target : targets[result[result.size() - 1]]) {
74
- if (target.second > 0 ) {
172
+ if (target.second > 0 ) { // 记录到达机场是否飞过了
75
173
result.push_back(target.first);
76
174
target.second--;
77
175
if (backtracking(ticketNum, index + 1, result)) return true;
@@ -81,24 +179,23 @@ backtracking() {
81
179
}
82
180
```
83
181
84
- 可以看出 通过` unordered_map<string, map<string, int>> targets ` 里的int字段来判断 这个集合使用使用完了,这样避免了 增删元素 。
182
+ 可以看出 通过` unordered_map<string, map<string, int>> targets ` 里的int字段来判断 这个集合里的机场是否使用过,这样避免了直接去删元素 。
85
183
86
- 此时完整代码如下 :
184
+ 分析完毕,此时完整C++代码如下 :
87
185
88
186
# C++代码
89
187
90
188
```
91
-
92
189
class Solution {
93
190
private:
94
- // unordered_map<出发城市 , map<到达城市 , 航班次数>> targets
191
+ // unordered_map<出发机场 , map<到达机场 , 航班次数>> targets
95
192
unordered_map<string, map<string, int>> targets;
96
193
bool backtracking(int ticketNum, vector<string>& result) {
97
194
if (result.size() == ticketNum + 1) {
98
195
return true;
99
196
}
100
197
for (pair<const string, int>& target : targets[result[result.size() - 1]]) {
101
- if (target.second > 0 ) { // 使用int字段来记录到达城市是否使用过了
198
+ if (target.second > 0 ) { // 记录到达机场是否飞过了
102
199
result.push_back(target.first);
103
200
target.second--;
104
201
if (backtracking(ticketNum, result)) return true;
@@ -110,16 +207,31 @@ bool backtracking(int ticketNum, vector<string>& result) {
110
207
}
111
208
public:
112
209
vector<string> findItinerary(vector<vector<string>>& tickets) {
210
+ targets.clear();
113
211
vector<string> result;
114
212
for (const vector<string>& vec : tickets) {
115
213
targets[vec[0]][vec[1]]++; // 记录映射关系
116
214
}
117
- result.push_back("JFK");
215
+ result.push_back("JFK"); // 起始机场
118
216
backtracking(tickets.size(), result);
119
217
return result;
120
218
}
121
219
};
122
-
123
220
```
124
221
222
+ 一波分析之后,可以看出我就是按照回溯算法的模板来的。
223
+
224
+ # 总结
225
+
226
+ 本题其实可以算是一道hard的题目了,关于本题的难点我在文中已经列出了。
227
+
228
+ ** 如果单纯的回溯搜索(深搜)并不难,难还难在容器的选择和使用上** 。
229
+
230
+ 本题其实是一道深度优先搜索的题目,但是我完全使用回溯法的思路来讲解这道题题目,** 算是给大家拓展一下思维方式,其实深搜和回溯也是分不开的,毕竟最终都是用递归** 。
231
+
232
+ 如果最终代码,发现照着回溯法模板画的话好像也能画出来,但难就难如何知道可以使用回溯,以及如果套进去,所以我再写了这么长的一篇来详细讲解。
233
+
234
+ 就酱,很多录友表示和「代码随想录」相见恨晚,那么帮Carl宣传一波吧,让更多同学知道这里!
235
+
236
+
125
237
> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。
0 commit comments