Leetcode(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
  • fromi 和 toi 由大写英文字母组成
  • fromi != toi

题解

方法一:Hierholzer 算法 + 贪心

思路

​​  先将题目重新定义:给定一个 n n n 个点 m m m 条边的图,要求从指定的顶点出发,经过所有的边恰好一次(可以理解为给定起点的「一笔画」问题),且使得路径的字典序最小。
​​  而这种「一笔画」问题与欧拉图或者半欧拉图有着紧密的联系。因为本题保证至少存在一种合理的路径,也就告诉了我们,这张图至少具有一条欧拉路径,而我们只需要输出这条欧拉通路的路径即可,这表示我们可以使用 Hierholzer 算法。

​​  既然要求字典序最小,那么我们每次应该贪心地选择当前顶点所连的顶点中字典序最小的那一个,并将其入栈。最后栈中就保存了我们遍历的顺序——即机票的行程组合。
​​  为了保证我们能够快速找到和当前顶点所连的顶点中字典序最小的那一个,我们可以使用优先队列存储当前顶点可以移动到的顶点,每次我们 O ( 1 ) O(1) O(1) 地找到最小字典序的顶点,并 O ( log ⁡ m ) O(\log m) O(logm) 地删除它( m m m 指以当前顶点为起飞机场的机票的数量,即当前顶点可以移动到的顶点个数)。

算法实现:

  1. 数据结构:用 vector 代替栈,以方便实现最后的逆序;采用优先队列存储可移动的下一顶点并按字典序令最小的队顶;采用哈希表存储机票,以起飞地为关键字,优先队列为值以方便查找。
  2. 算法(即 Hierholzer 算法):使用递归实现 DFS 算法,并使用 while 语句以实现回溯,并采取逆序入栈的方式。
代码实现
class Solution {
public:
	// 采用优先队列存储可移动的下一顶点并按字典序令最小的在顶
    unordered_map<string, priority_queue<string, vector<string>, std::greater<string>> > vec;
    // 用 vector 代替栈,以简单实现最后的逆序
    vector<string> stk;

    void DFS(const string& curr) {
	    string tmp;
	    // 为什么是 while 而不是 if 呢?因为这是为了在进入死胡同时回溯到上一个顶点
        while(vec.count(curr) && vec[curr].size() > 0){	
            tmp = vec[curr].top();
            vec[curr].pop();
            DFS(move(tmp));	// 获得 tmp 的右值引用
        }
        stk.emplace_back(curr); // 遍历完这个顶点连接的所有顶点之后才入栈
    }

    vector<string> findItinerary(vector<vector<string>>& tickets) {
        for(auto& it : tickets){
        	// priority_queue<string, vector<string>, std::greater<string>> tmp(t[1]); 
        	// 不可以,没有其含参的构造函数,只有一个编译器合成的默认构造函数
            // vec.emplace(it[0], priority_queue<string, vector<string>, std::greater<string>>()).first->second.push(it[1]);
            vec[it[0]].emplace(it[1]);	// unordered_map 的重载运算符 [] 在没找到当前关键字时会将其插入并返回关键值对的 second
            // 使用重载运算符 [] 的效率要高些,避免了默认初始化临时优先队列和用其拷贝初始化
        }
        DFS("JFK");
        reverse(stk.begin(), stk.end());	// 逆序
        return stk;
    }
};
复杂度分析

时间复杂度: O ( m log ⁡ m ) O(m \log m) O(mlogm),其中 m m m 是边(即机票)的数量。对于每一条边我们需要 O ( log ⁡ m ) O(\log m) O(logm) 地删除它,最终的答案序列长度为 m + 1 m+1 m+1,而与 n n n 无关( n n n 是机场的个数,即图中顶点的个数)
空间复杂度: O ( m ) O(m) O(m),其中 m m m 是边(即机票)的数量

方法二:DFS + 回溯算法 + 贪心

思路

​​  我们可以采取 DFS + 回溯算法 + 贪心的组合,可能你会觉得这个 Hierholzer 算法很像,这是因为 Hierholzer 算法也是用到了 DFS ,只是它们在回溯时的处理不太一样。

算法实现:

  1. 数据结构:一个城市映射多个城市,城市之间要靠字典序排列,一个城市映射多个城市,可以使用 std::unordered_map,如果让多个城市之间有顺序的话,就是用 std::map 或者 std::multiset。而我们选择 std::map ,因为使用 unordered_map<string, multiset<string>> targets 遍历 multiset 的时候,不能删除元素,一旦删除元素,迭代器就失效了。而本地在回溯的过程中就是要不断的增删 multiset 里的元素,所以我们使用 unordered_map<string, map<string, int>> targets。在遍历 unordered_map<出发城市, map<到达城市, 航班次数>> targets 的过程中,可以使用航班次数这个字段的数字 – 或者 ++,来标记到达城市是否使用过了,而不用对集合做删除元素或者增加元素的操作。
  2. 所以终止条件是:在回溯遍历的过程中,遇到的机场个数如果等于航班数量 +1,那么我们就找到了一个行程,并把所有航班都使用了且只使用一次。
代码实现
class Solution {
private:
    // unordered_map<始飞地, map<目的地, 航班次数>> 其中航班次数为0表示没有这种机票,为1表示有1张,2表示有2张,以此类推
    // 通过哈希表实现快速查找,通过map实现快速取最值和方便回溯时进行撤销
    unordered_map<string, map<string, int>> ticket; 
    vector<string> ans;
    int nums;   // 机票+1
    bool DFS(const string& P){  // true 会回溯,false 不会
        ans.push_back(P);
        if(ticket.count(P) != 0){
            for(auto& it: ticket[P]){
                if(it.second == 0){
                    continue;
                }else{
                    it.second--;
                    if(DFS(it.first)){  // true 则回溯
                        it.second++;
                    }
                }
            }
        }
        if(ans.size() == nums)
            return false;
        else{
            ans.pop_back();
            return true;
        }
    }

public:
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        // 初始化
        for(auto& it: tickets){
            if(ticket.count(it[0]) == 0)
                ticket[it[0]][it[1]] = 1;
            else ++ticket[it[0]][it[1]];
        }
        nums = tickets.size()+1;
        DFS("JFK");
        return ans;
    }
};
复杂度分析

时间复杂度: O ( m log ⁡ m ) O(m \log m) O(mlogm),其中 m m m 是边(即机票)的数量。对于每个顶点的每一条边我们需要 O ( log ⁡ m ) O(\log m) O(logm) 地创建并访问它,最终的答案序列长度为 m + 1 m+1 m+1,而与 n n n 无关( n n n 是机场的个数,即图中顶点的个数)
空间复杂度: O ( m ) O(m) O(m),其中 m m m 是边(即机票)的数量

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
KMP算法是一种字符串匹配算法,用于在一个文本串S内查找一个模式串P的出现位置。它的时间复杂度为O(n+m),其中n为文本串的长度,m为模式串的长度。 KMP算法的核心思想是利用已知信息来避免不必要的字符比较。具体来说,它维护一个next数组,其中next[i]表示当第i个字符匹配失败时,下一次匹配应该从模式串的第next[i]个字符开始。 我们可以通过一个简单的例子来理解KMP算法的思想。假设文本串为S="ababababca",模式串为P="abababca",我们想要在S中查找P的出现位置。 首先,我们可以将P的每个前缀和后缀进行比较,得到next数组: | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | --- | - | - | - | - | - | - | - | - | | P | a | b | a | b | a | b | c | a | | next| 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 | 接下来,我们从S的第一个字符开始匹配P。当S的第七个字符和P的第七个字符匹配失败时,我们可以利用next[6]=4,将P向右移动4个字符,使得P的第五个字符与S的第七个字符对齐。此时,我们可以发现P的前五个字符和S的前五个字符已经匹配成功了。因此,我们可以继续从S的第六个字符开始匹配P。 当S的第十个字符和P的第八个字符匹配失败时,我们可以利用next[7]=1,将P向右移动一个字符,使得P的第一个字符和S的第十个字符对齐。此时,我们可以发现P的前一个字符和S的第十个字符已经匹配成功了。因此,我们可以继续从S的第十一个字符开始匹配P。 最终,我们可以发现P出现在S的第二个位置。 下面是KMP算法的C++代码实现:

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值