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

文章目录

332. 重新安排行程

题目描述

给你一份航线列表 tickets ,其中 ticketsi = 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
  • ticketsi.length == 2
  • fromi.length == 3
  • toi.length == 3
  • fromi 和 toi 由大写英文字母组成
  • fromi != toi

思路

这道题目还是很难的,之前我们用回溯法解决了如下问题:组合问题 (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 是不可修改了,这就是语法规定了。


相关推荐
晚笙coding15 小时前
LeetCode 226. 翻转二叉树(Invert Binary Tree)
算法·leetcode·职场和发展
退休倒计时19 小时前
【每日一题】LeetCode 287. 寻找重复数 TypeScript
算法·leetcode·typescript
G.O.G.O.G20 小时前
《LeetCode SQL 从入门到精通(MySQL)》09
sql·mysql·leetcode
To_OC2 天前
LC 22 括号生成:刷完这道题,我终于搞懂回溯剪枝了
javascript·算法·leetcode
To_OC2 天前
LC 39 组合总和:回溯入门必刷题,我踩过的两个坑都在这了
javascript·算法·leetcode
兰令水2 天前
hot100【acm版】【2026.7.14打卡-java版本】
java·数据结构·算法·leetcode·面试
tachibana22 天前
hot100 课程表(207)
java·数据结构·算法·leetcode
旖-旎2 天前
《LeetCode 646 最长数对链 || LeetCode 1143 最长公共子序列》
c++·算法·leetcode·动态规划
Frostnova丶2 天前
(15)LeetCode 189. 轮转数组
数据结构·算法·leetcode
伊玛目的门徒2 天前
试用leetcode之典中典 二数之和问题
java·算法·leetcode