题外话:四天时间刷完回溯算法,不知道这种速度算不算快哈哈哈,不过对于第一次接触回溯的小白来说,很多题目都很烧脑,重复行程坐飞机这道题把我卡了一个晚上
回溯算法
一种搜索方式,回溯是递归的副产品,只要有递归就会有回溯,回溯的本质是穷举所有可能
涉及题目类型
- 组合:N个数里面按一定规则找出k个数的集合
- 分割:一个字符串按一定规则有几种切割方式
- 子集:一个N个数的集合里有多少符合条件的子集
- 排列:N个数按一定规则全排列,有几种排列方式
- 棋盘:N皇后,解数独等等
区别:组合不强调顺序,排列强调顺序
回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度,回溯法解决的问题都可以抽象为树形结构(N叉树)
卡神的模板真的强
void backtracking(参数) {//模板
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
一些知识点
substr(startIndex,i - startIndex + 1);/
/从startIndex开始,截取长度为i-startInedex+1的部分字符
private代表类内私有成员,仅有类内函数可以访问private,类外一切函数(包括继承者)均不可访问private成员。
求组合的情况
- 一个集合来求组合的话,就需要startIndex,如1,12,123,2,23,3
- 多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,如**电话号码的字母组合**
在求和问题中,排序之后加剪枝是常见的套路!
重新安排行程
查了不少人写的注释说明,确实很不好理解,其中target指的就是map的value,即target.first也是个目的地,value是票的数量
//重新安排行程
class Solution {
private:
unordered_map<string, map<string, int>> targets;
// ticketNum是一共有多少张机票,result是最终的飞行路线结果
bool backtracking(int ticketNum, vector<string> &result)
{
// 递归终止条件:如果飞行路线中机场个数 = 机票张数 + 1,则说明把所有机票都用完了,
// 即收集到了一个可能路线
if (result.size() == ticketNum + 1)
{
return true;
}
// 开始递归:从上一次到达的机场,或者最初起飞的机场出发),遍历所有它可能飞往的达到机场,寻找一条路径
for (pair<const string, int> &target : targets[result[result.size() - 1]])
{//target可以理解为一个map
// 如果从当前机场,飞往到达机场的机票还有剩余,那么就还能飞
if (target.second!=0)//还有目的地的飞机票
{
result.push_back(target.first); // 飞往到达机场,加到路线中
target.second--; // 当前机场->到达机场,所以机票张数-1
// 重要:递归调用,从下一个机场作为起飞点,再次寻找飞往的目的地
// (1)如果调用下一个机场的结果是true,说明找到了一条路线,则直接返回
// (2)如果调用下一个机场的结果是false,说明没有找到一条完整路线(即走到了死胡同里)
// 那么就到了下面回溯的过程:即把当前for循环选择的到达机场弹出,换另一个到达机场
if (backtracking(ticketNum, result))
return true; // 找到一条路线就返回
result.pop_back(); // 回溯
// 注意:这里一定不要忘了机票数量++,因为上面--是因为当前位置的到达机场为当前for循环选择的值
// (假设是B)的时候,后面调用递归安排后面的路线。一旦后面递归完成,说明当前位置选择
// 这个for循环的值B经过测试不能满足要求了,所以要把当前选择的到达机场B弹出,然后把
// 它的机票张数也复原,然后重新选择另一个达到机场C,此时B的机票张数应该还是原来的张数
// 所以这里回溯的时候一定要把机票张数也++,对机票张数进行复原,说不定后面又有路线到B
target.second++;
}
}
return false;
}
public:
vector<string> findItinerary(vector<vector<string>>& tickets) {
vector<string> result;
//将tickets映射成有一个起始机场对应不同终点机场的情况
//unordered_map--key无序 map--key有序
// vec[0]是出发机场的名称,targets[vec[0]]就是取出键对应的值,即<到达机场,剩余机票次数>
for(const vector<string>& vec:tickets){
//一种特殊写法,找出的这个数值对叫vec,每一次都更新,比较新的写法
targets[vec[0]][vec[1]]++;由vec[0]起飞 vec[1]降落的飞机增加一个架次
}
result.push_back("JFK");
backtracking(tickets.size(),result);
return result;
}
};
N皇后——不能同行,不能同列,不能同斜线
每一行每一列只放一个皇后,只需要一层for循环遍历一行,递归来遍历列,然后一行一列确定皇后的唯一位置,不可能有同样情况,所以不用去重。
递归函数的返回值需要是bool类型,为什么呢?
用void声明可以返回插入多个目标,如N皇后的多种解法,但bool只需要有一种解法即可
bool类型返回值只有true或者false,而void只是执行这段函数,没有任何返回值
如果是一个集合来求组合加粗样式的话,就需要startIndex,这是因为要去重
如(0,1)和(1,0)是一个东西,后面的数不能再用前面的数。
求排列会用到used(方便些)used数组在时间复杂度上几乎没有额外负担!
放一张网友总结的思维导图,希望二刷这些题还能记得哈哈哈