回溯算法:leetcode 332.重新安排行程、51.N皇后、37.解数独

leetcode 332.重新安排行程

leetcode 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"]。但是它自然排序更大更靠后。

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

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

这道题目有几个难点:

  1. 一个行程中,如果航班处理不好容易变成一个圈,成为死循环

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

  1. 使用回溯法(也可以说深搜) 的话,那么终止条件是什么呢?

  1. 搜索的过程中,如何遍历一个机场所对应的所有机场。

死循环问题

如上述示例二:[["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]中的["JFK","ATL"]和["ATL","JFK"],如果处理不好的话会出现死循环的情况。但是出发机场和到达机场是可以重复的。

记录映射关系

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

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

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

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

回溯三部曲

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

  1. 确定递归函数的参数和返回值

使用unordered_map<string, map<string, int>> targets; 来记录航班的映射关系,我定义为全局变量。参数里还需要ticketNum,表示有多少个航班(终止条件会用上)。

还要定义一个全局变量vector<string> result,本题的result相当于之前题目的path,我们只需找到一条合适的路径就可以了。

递归函数的返回值为bool型,因为我们只需要找到一个行程,就是在树形结构中唯一的一条通向叶子节点的路线。

vector<string> result;
unordered_map<string, map<string, int>> targets;
bool backtracking(int ticketNum, vector<string>& result)
  1. 确定终止条件

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

if(result.size() == ticketNum + 1){
    return true;
}
  1. 单层搜索的逻辑

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

遍历过程如下:

// pair里要有const,因为map中的key是不可修改的,所以是pair<const string, int>
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;
        target.second++;
        result.pop_back();
    }
}

整体代码如下:

class Solution {
private:
    vector<string> result;
    unordered_map<string, map<string, int>> targets;
    bool backtracking(int ticketNum, vector<string>& result){
        if(result.size() == ticketNum + 1){
            return true;
        }
        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;
                target.second++;
                result.pop_back();
            }
        }
        return false;
    }
public:
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        for(const vector<string>& vec : tickets){
            targets[vec[0]][vec[1]]++;
        }
        result.push_back("JFK");
        backtracking(tickets.size(), result);
        return result;
    }
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值