332. 重新安排行程
思路
题目本质是要在机票列表中找到符合条件的组合/排列,因此可以使用回溯来解题
注意:结果是有排序要求的,要求返回从小到大最小的结果
解题方法
在回溯的横向遍历中,有两种解法
- 直接 for 循环暴力遍历所有的机票,在机票较多时容易超时
- 使用哈希表重构机票列表,构建
出发机场 -> (到达机场 & 航班次数)
哈希表,提高遍历效率
无论哪种解法,在遍历之前都需要将待遍历对向进行排序才能保证最终的 path
正确
复杂度
- 时间复杂度:
添加时间复杂度, 示例: O ( n ) O(n) O(n)
- 空间复杂度:
添加空间复杂度, 示例: O ( n ) O(n) O(n)
Code
暴力检索
class Solution {
private LinkedList<String> path = new LinkedList<>();
// 指示到过的目的地,避免进入死循环
private boolean[] used;
public List<String> findItinerary(List<List<String>> tickets) {
used = new boolean[tickets.size()];
// 对 tickets 的**终点**进行升序排序,保证最先找到的路径是字典排序最小的
Collections.sort(tickets, (a, b) -> a.get(1).compareTo(b.get(1)));
// 起始位置为 "JFK"
path.add("JFK");
backTracking(tickets);
// 只需要一个路径结果
return path;
}
private boolean backTracking(List<List<String>> tickets) {
// 终止条件
// 路径已经规划完成,整个流程结束,也即回溯到一条符合的路径后,整个回溯递归需要终止
if (path.size() == tickets.size() + 1) {
return true;
}
// 采用暴力的横向遍历,每一层都要对所有的机票进行遍历判断,最终会在第 80 个测试用例中超时
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;
} else {
continue;
}
boolean hasRes = backTracking(tickets);
if (hasRes) {
return true;
}
used[i] = false;
path.removeLast();
}
return false;
}
}
哈希表优化
class Solution {
private LinkedList<String> path = new LinkedList<>();
// Map<出发机场, Map<到达机场, 航班次数>>
// 内嵌 Map 使用 TreeMap,维持大顶堆,这样顶上的就是最小的几个
private Map<String, Map<String, Integer>> target = new HashMap<String, Map<String, Integer>>();
private int ticketNum;
public List<String> findItinerary(List<List<String>> tickets) {
// 把 tickets 的所有航班信息转换为:Map<出发机场, Map<到达机场, 航班次数>> 的 Map 集合
for (List<String> t: tickets) {
// 每个出发机场放入 target,并构建 TreeMap 放所有从这个机场出发的机票
if (!target.containsKey(t.get(0))) {
Map<String, Integer> temp = new TreeMap<>();
temp.put(t.get(1), 1);
target.put(t.get(0), temp);
}
// 追加
else {
Map<String, Integer> temp = target.get(t.get(0));
temp.put(t.get(1), temp.getOrDefault(t.get(1), 0) + 1);
}
}
// 起始机场
path.add("JFK");
ticketNum = tickets.size();
backTracking();
return path;
}
private boolean backTracking() {
// 终止条件,路径包含完整行程即可退出整个流程
if (path.size() == ticketNum + 1) {
return true;
}
// 横向遍历
// 使用哈希表替代暴力检索,避免判断不可能到达的目的地
if (target.containsKey(path.getLast())) {
// 所有可能目的地,因为使用的TreeMap 大顶堆,所以遍历时是从小到大遍历
for (Map.Entry<String, Integer> t: target.get(path.getLast()).entrySet()) {
// 当前目的地所余票数
Integer cnt = t.getValue();
if (cnt > 0) {
t.setValue(--cnt);
path.add(t.getKey());
if (backTracking()) {
return true;
}
t.setValue(++cnt);
path.removeLast();
}
}
}
return false;
}
}