332.重新安排行程
给定一个机票的字符串二维数组 [from, to],子数组中的两个成员分别表示飞机出发和降落的机场地点,对该行程进行重新规划排序。所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。
提示:
- 如果存在多种有效的行程,请你按字符自然排序返回最小的行程组合。例如,行程 ["JFK", "LGA"] 与 ["JFK", "LGB"] 相比就更小,排序更靠前
- 所有的机场都用三个大写字母表示(机场代码)。
- 假定所有机票至少存在一种合理的行程。
- 所有的机票必须都用一次 且 只能用一次。
示例 1:
- 输入:[["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]]
- 输出:["JFK", "MUC", "LHR", "SFO", "SJC"]
示例 2:
- 输入:[["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
- 输出:["JFK","ATL","JFK","SFO","ATL","SFO"]
- 解释:另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"]。但是它自然排序更大更靠后。
#算法公开课
《代码随想录》算法视频公开课 (opens new window):带你学透回溯算法(理论篇) (opens new window),相信结合视频再看本篇题解,更有助于大家对本题的理解。
#思路
这道题目还是很难的,之前我们用回溯法解决了如下问题:组合问题 (opens new window),分割问题 (opens new window),子集问题 (opens new window),排列问题 (opens new window)。
直觉上来看 这道题和回溯法没有什么关系,更像是图论中的深度优先搜索。
实际上确实是深搜,但这是深搜中使用了回溯的例子,在查找路径的时候,如果不回溯,怎么能查到目标路径呢。
所以我倾向于说本题应该使用回溯法,那么我也用回溯法的思路来讲解本题,其实深搜一般都使用了回溯法的思路,在图论系列中我会再详细讲解深搜。
这里就是先给大家拓展一下,原来回溯法还可以这么玩!
这道题目有几个难点:
- 一个行程中,如果航班处理不好容易变成一个圈,成为死循环
- 有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ?
- 使用回溯法(也可以说深搜) 的话,那么终止条件是什么呢?
- 搜索的过程中,如何遍历一个机场所对应的所有机场。
针对以上问题我来逐一解答!
#如何理解死循环
对于死循环,我来举一个有重复机场的例子:
为什么要举这个例子呢,就是告诉大家,出发机场和到达机场也会重复的,如果在解题的过程中没有对集合元素处理好,就会死循环。
#该记录映射关系
有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ?
一个机场映射多个机场,机场之间要靠字母序排列,一个机场映射多个机场,可以使用std::unordered_map,如果让多个机场之间再有顺序的话,就是用std::map 或者std::multimap 或者 std::multiset。
如果对map 和 set 的实现机制不太了解,也不清楚为什么 map、multimap就是有序的同学,可以看这篇文章关于哈希表,你该了解这些! (opens new window)。
这样存放映射关系可以定义为 unordered_map<string, multiset<string>> targets
或者 unordered_map<string, map<string, int>> targets
。
含义如下:
unordered_map<string, multiset> targets:unordered_map<出发机场, 到达机场的集合> targets
unordered_map<string, map<string, int>> targets:unordered_map<出发机场, map<到达机场, 航班次数>> targets
这两个结构,我选择了后者,因为如果使用unordered_map<string, multiset<string>> targets
遍历multiset的时候,不能删除元素,一旦删除元素,迭代器就失效了。
再说一下为什么一定要增删元素呢,正如开篇我给出的图中所示,出发机场和到达机场是会重复的,搜索的过程没及时删除目的机场就会死循环。
所以搜索的过程中就是要不断的删multiset里的元素,那么推荐使用unordered_map<string, map<string, int>> targets
。
在遍历 unordered_map<出发机场, map<到达机场, 航班次数>> targets
的过程中,可以使用"航班次数"这个字段的数字做相应的增减,来标记到达机场是否使用过了。
如果“航班次数”大于零,说明目的地还可以飞,如果“航班次数”等于零说明目的地不能飞了,而不用对集合做删除元素或者增加元素的操作。
相当于说我不删,我就做一个标记!
题解: 这个题的难点一个是用什么容器来接纳机场地址,然后还能给这个进行排序,熟悉Collection的用法还有lamda函数很重要。然后就是用一个used数组来对已经用过的机票进行标记。
class Solution {
LinkedList<String> result;
LinkedList<String> path = new LinkedList<>();
public List<String> findItinerary(List<List<String>> tickets) {
Collections.sort(tickets,(a,b)->a.get(1).compareTo(b.get(1)));
path.add("JFK");
boolean[] used = new boolean[tickets.size()];
Arrays.fill(used,false);
callTraverse((ArrayList)tickets,used);
return result;
}
private boolean callTraverse(ArrayList<List<String>> tickets, boolean[] used){
if(path.size() == tickets.size()+1){
result = path;
return true;
}
for(int i = 0; i < tickets.size();i++){
if(!used[i] && tickets.get(i).get(0).equals(path.getLast())){
path.add(tickets.get(i).get(1));
used[i] = true;
if(callTraverse(tickets,used)){
return true;
}
path.removeLast();
used[i] = false;
}
}
return false;
}
}