332. 重新安排行程(力扣LeetCode)

文章目录

332. 重新安排行程

题目描述

给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。

所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。

例如,行程 ["JFK", "LGA"] 与 ["JFK", "LGB"] 相比就更小,排序更靠前。

假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。

示例 1:

输入:tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]]

输出:["JFK","MUC","LHR","SFO","SJC"]

示例 2:

输入:tickets = [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]

输出:["JFK","ATL","JFK","SFO","ATL","SFO"]

解释:另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"] ,但是它字典排序更大更靠后。

提示:

  • 1 <= tickets.length <= 300
  • tickets[i].length == 2
  • fromi.length == 3
  • toi.length == 3
  • from~i~ 和 to~i~ 由大写英文字母组成
  • from~i~ != to~i~

思路

这道题目还是很难的,之前我们用回溯法解决了如下问题:组合问题 (opens new window),分割问题 (opens new window),子集问题 (opens new window),排列问题 (opens new window)。

直觉上来看 这道题和回溯法没有什么关系,更像是图论中的深度优先搜索。

实际上确实是深搜,但这是深搜中使用了回溯的例子,在查找路径的时候,如果不回溯,怎么能查到目标路径呢。

所以我倾向于说本题应该使用回溯法,那么我也用回溯法的思路来讲解本题,其实深搜一般都使用了回溯法的思路,在图论系列中我会再详细讲解深搜。

这里就是先给大家拓展一下,原来回溯法还可以这么玩!

这道题目有几个难点:

  1. 一个行程中,如果航班处理不好容易变成一个圈,成为死循环
  2. 有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ?
  3. 使用回溯法(也可以说深搜) 的话,那么终止条件是什么呢?
  4. 搜索的过程中,如何遍历一个机场所对应的所有机场。

针对以上问题我来逐一解答!

如何理解死循环

对于死循环,我来举一个有重复机场的例子:

为什么要举这个例子呢,就是告诉大家,出发机场和到达机场也会重复的,如果在解题的过程中没有对集合元素处理好,就会死循环。

该记录映射关系

有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ?

一个机场映射多个机场,机场之间要靠字母序排列,一个机场映射多个机场,可以使用std::unordered_map,如果让多个机场之间再有顺序的话,就是用std::map 或者std::multimap 或者 std::multiset

如果对map 和 set 的实现机制不太了解,也不清楚为什么 map、multimap就是有序的同学,可以看这篇文章关于哈希表,你该了解这些!

这样存放映射关系可以定义为 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的过程中,可以使用"航班次数"这个字段的数字做相应的增减,来标记到达机场是否使用过了。

如果"航班次数"大于零,说明目的地还可以飞,如果"航班次数"等于零说明目的地不能飞了,而不用对集合做删除元素或者增加元素的操作。

相当于说我不删,我就做一个标记!

dfs(回溯法)

这道题目我使用回溯法,那么下面按照我总结的回溯模板来:

cpp 复制代码
void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

本题以输入:[["JFK", "KUL"], ["JFK", "NRT"], ["NRT", "JFK"]为例,抽象为树形结构如下:

开始回溯三部曲讲解:

  • 递归函数参数

在讲解映射关系的时候,已经讲过了,使用unordered_map<string, map<string, int>> targets; 来记录航班的映射关系,我定义为全局变量。

当然把参数放进函数里传进去也是可以的,我是尽量控制函数里参数的长度。

参数里还需要ticketNum,表示有多少个航班(终止条件会用上)。

代码如下:

cpp 复制代码
// unordered_map<出发机场, map<到达机场, 航班次数>> targets
unordered_map<string, map<string, int>> targets;
bool backtracking(int ticketNum, vector<string>& result) {

注意函数返回值我用的是bool!

我们之前讲解回溯算法的时候,一般函数返回值都是void,这次为什么是bool呢?

因为我们只需要找到一个行程,就是在树形结构中唯一的一条通向叶子节点的路线,如图:

所以找到了这个叶子节点了直接返回

当然本题的targets和result都需要初始化,代码如下:

cpp 复制代码
for (const vector<string>& vec : tickets) {
    targets[vec[0]][vec[1]]++; // 记录映射关系
}
result.push_back("JFK"); // 起始机场
  • 递归终止条件

拿题目中的示例为例,输入: [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]] ,这是有4个航班,那么只要找出一种行程,行程里的机场个数是5就可以了。

所以终止条件是:我们回溯遍历的过程中,遇到的机场个数,如果达到了(航班数量+1),那么我们就找到了一个行程,把所有航班串在一起了。

代码如下:

cpp 复制代码
if (result.size() == ticketNum + 1) {
    return true;
}
  • 单层搜索的逻辑

回溯的过程中,如何遍历一个机场所对应的所有机场呢?

这里刚刚说过,在选择映射函数的时候,不能选择unordered_map<string, multiset<string>> targets, 因为一旦有元素增删multiset的迭代器就会失效,当然可能有牛逼的容器删除元素迭代器不会失效,这里就不在讨论了。

可以说本题既要找到一个对数据进行排序的容器,而且还要容易增删元素,迭代器还不能失效。

所以我选择了unordered_map<string, map<string, int>> targets来做机场之间的映射。

遍历过程如下:

cpp 复制代码
for (pair<const string, int>& target : targets[result[result.size() - 1]]) {
    if (target.second > 0 ) { // 记录到达机场是否飞过了
        result.push_back(target.first);
        target.second--;
        if (backtracking(ticketNum, result)) return true;
        result.pop_back();
        target.second++;
    }
}

可以看出 通过unordered_map<string, map<string, int>> targets里的int字段来判断 这个集合里的机场是否使用过,这样避免了直接去删元素。

代码

下面是针对提供的C++代码的详细注释,解释了代码的功能以及每个重要部分的作用,旨在帮助理解如何重新安排航班行程的问题:

cpp 复制代码
class Solution {
public:
    // 主函数,用来调用回溯并返回最终的行程列表
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        // 遍历所有的票并记录每一对出发地和目的地之间的票数
        for(vector<string> i : tickets)
            tar[i[0]][i[1]]++;
        
        // 行程总是从JFK开始,所以先添加JFK到结果中
        res.push_back("JFK");
        // 调用回溯函数,尝试构建合理的行程
        back_stracking(tickets.size(),res);
        // 返回构建好的行程
        return res;
    }

private:
    // 用于存放最终结果行程的列表
    vector<string> res;
    // 使用一个映射来记录每一对出发地和目的地间的票数
    // 由于map默认按key排序,因此内部的map会根据字典序自动排序目的地
    unordered_map<string,map<string,int>> tar;
    
    // 回溯函数,用来尝试构建行程
    bool back_stracking(int num, vector<string>& res) {
        // 如果行程的长度等于所有票的数量+1(因为最终的行程会比票的数量多一个出发地),则找到了一个有效的行程
        if(res.size() == num + 1)
            return true;
        // 遍历当前出发地到不同目的地的所有票
        for(pair<const string,int>& tics : tar[res[res.size()-1]]) {
            // 如果当前有剩余的票,则尝试这个目的地
            if(tics.second > 0) {
                // 使用一张票,并将目的地添加到行程中
                tics.second--;
                res.push_back(tics.first);
                // 继续回溯,如果返回true,说明找到了有效行程,直接返回true
                if(back_stracking(num,res)) return true;
                // 如果没有找到有效行程,回溯,即撤销刚才的选择
                res.pop_back();
                tics.second++;
            }
        }
        // 如果所有票都尝试过仍未找到有效行程,则返回false
        return false;
    }
};

这段代码的核心思想是使用深度优先搜索(DFS)结合回溯来寻找所有可能的行程,并利用map的自动排序特性来保证行程按字典序排列。每尝试一种行程,都会从相应的出发地-目的地对中减去一张票,并递归搜索。如果最终构成的行程长度正确,则说明找到了一个符合条件的行程。如果某条路径无法构成有效行程,则会撤销这一选择(即回溯)并尝试其他可能的目的地。这个过程会一直持续,直到找到一个有效的行程或者所有的行程都尝试完毕。


代码中

cpp 复制代码
for (pair<const string, int>& target : targets[result[result.size() - 1]])

一定要加上引用即 & target,因为后面有对 target.second 做减减操作,如果没有引用,单纯复制,这个结果就没记录下来,那最后的结果就不对了。

加上引用之后,就必须在 string 前面加上 const,因为map中的key 是不可修改了,这就是语法规定了。


相关推荐
虽千万人 吾往矣2 小时前
golang LeetCode 热题 100(动态规划)-更新中
算法·leetcode·动态规划
姚先生975 小时前
LeetCode 209. 长度最小的子数组 (C++实现)
c++·算法·leetcode
HUT_Tyne2656 小时前
力扣--LCR 53.最大数组和
算法·leetcode·动态规划
南宫生6 小时前
力扣-数据结构-1【算法学习day.72】
java·数据结构·学习·算法·leetcode
chenziang16 小时前
leetcode hot100 删除链表的第n个节点
算法·leetcode·链表
清炒孔心菜6 小时前
每日一题 342. 4的幂
leetcode
刚学HTML9 小时前
leetcode 05 回文字符串
算法·leetcode
冠位观测者9 小时前
【Leetcode 每日一题】2545. 根据第 K 场考试的分数排序
数据结构·算法·leetcode
古希腊掌管学习的神10 小时前
[LeetCode-Python版]相向双指针——611. 有效三角形的个数
开发语言·python·leetcode
DARLING Zero two♡12 小时前
【优选算法】Pointer-Slice:双指针的算法切片(下)
java·数据结构·c++·算法·leetcode