基本思路
前序:
在搞清楚基本思路之前,我们先根据每个回溯题目的大概思路,思考一下回溯法的难点在哪里。
难点1:回溯函数的参数的决定
回溯函数能如何进行下一步最关键的就是参数要怎么变化,找到每一步有哪些参数进行改变了,进行下一步要改变的参数又是拿些。这就可以确定了回溯函数的参数
难点2:每一步回溯函数的逻辑
在一步的回溯法中,要找到这步的选择列表,然后思考要不要使用到后面回溯的结果,最后再决定每个数要进行操作
难点3:回溯法的回溯操作
回溯操作就是回归到上一步的状态。例如在数组中就删除最后进去的数,在计算总和就减去最后的数等等。这步实际不难,但是很容易忘记
难点4:剪枝
剪枝就是判断一些条件,让搜索提前结束,让效率变高。重点就是条件的寻找,有些题的关键就是找到剪枝的条件。没有基本的规律,需要根据题意来寻找
几种算法方法得区别
1.递归法:从最典型的递归例子斐波纳契数列可以看出,递归就是实体自己和自己建立关系。
2.回溯法:以某种方式一直计算,计算到无法计算的时候,就返回上一个状态,继续按着另一种方式计算,直到无法计算且全部方式都计算过以后
3.贪心算法:把问题分解成子问题,通过子问题的局部最优解求出问题的最优解。问题必须具有无后效性才能使用贪心算法
4.动态规划法:重叠子问题和最优解是dp的最重要两个性质
例题1:给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
实例:[1 , 2 , 3]
1.找到递归的规律(画出递归树)
画出递归树,可以让我们大概了解每一步的逻辑,然后就是直观的看到每步的选择列表
2.确定选择列表
选择列表就是在进行这步的过程中,可以选择的数值是哪些。选择列表很重要,以后会根据选择列表来决定回溯函数的参数和每一步的逻辑。
3.找到回溯函数的参数
回溯函数参数根据选择列表判断每一步那些参数有变化,在选择列表中就是对数组进行遍历,每一步的变化也是遍历数组的索引值而已,所以这题的参数就是一个数组的遍历索引值,设为start
4.找到结束条件
这题要寻找的就是全部可能的子集,所以结束条件就是数组遍历完。
5.每一步的逻辑
要找到全部的子集,再根据选择列表,每一步就是把索引为start的数加入结果列表中即可
6.剪枝
这里没有重复的步骤,所以不需要剪枝
7.进行下一步
和上面说的一样,进行下一步就是把start索引值+1即可
8.回溯操作
数组的回溯操作就是删除最后进去的数
代码:
result = []
void backtrack(路径, 选择列表):
if (满足结束条件) {
result.push_back(路径);
return;
}:
for 选择 in 选择列表 {
做选择
backtrack(路径, 选择列表)
撤销选择
}
作者:labuladong
链接:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-xiang-jie-by-labuladong-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
总结:
每次做回溯题目的时候就按着这些步骤来完成,可能有不完善,但是起码可以提供一个基本思路:
- 画出递归树
- 找出选择列表
- 确定函数参数
- 找出结束条件
- 每一步的逻辑
- 找出剪枝条件
- 递归下一步
- 回溯操作
下面就是leetcode上的回溯法题目练习记录,花了一大段时间来总结基本思路,因为回溯法确实是不简单,很难找到入手点,提供一个基本思路,以后做题就不用无从下手了
分割回文串
思路:
1.画出递归树
2.选择列表
选择列表就是遍历整个string
3.函数参数
遍历string,判断回文串,所以参数就只有一个string索引值
4.结束条件
当string被遍历完就结束回溯函数
5.每一步逻辑
先使用substr来获取每一个子串,然后再判断子串是否是回文串,如果是则继续递归进行下一步
6.剪枝
没有重复步骤,所以不需要剪枝
7.递归下一步
把索引值 + 1即可进行下一步
8.回溯操作
直接删除最近进去的数即可
代码:
class Solution {
int sz;
vector<vector<string>> res;
public:
vector<vector<string>> partition(string s) {
sz = s.size();
if(!sz) return res;
vector<string> temp;
backtrack(s , 0 , temp);
return res;
}
bool isPalin(string &tmp , int l , int r) {
while(l <= r) {
if(tmp[l] != tmp[r]) return false;
l++;
r--;
}
return true;
}
void backtrack(string s , int start , vector<string> &temp) {
if(start >= sz) {
res.push_back(temp);
return;
}
for(int i = start; i < sz; i++) {
if(isPalin(s , start , i)) {
temp.push_back(s.substr(start , i - start + 1));
backtrack(s , i + 1 , temp);
temp.pop_back();
}
}
}
};
有重复字符串的排列组合
思路:
1.递归树
2.选择列表
就是遍历整个数组
3.函数参数
变化得参数只有索引值
4.结束条件
这题得不是需要全部子集,而是全集不同得排列方式,所以当数量达到全集元素的数量时,我们就结束函数
5.每一步的逻辑
直接把当前的char加到temp中,进行下一步即可
6.剪枝
剪枝时这题的关键,参考递归树中的红色方框,这四个方块两两都是重复的,说明当前一步的数和后面的数值相同时,我们就直接剪枝即可。
这里有两种情况剪枝:
- 就是当这个数在前面循环中已经重复过了的,就直接跳过
- 当这个数与前一个数相等,且前一个数的标识位变成false,证明前一个数已经经过一次计算了,就剪枝
7.递归下一步
没啥说的,直接带着改变后的temp进行下一步即可
8.回溯操作
这里的回溯只要把标识位设置为原本状态即可,因为我们是在进行下一步递归函数中完成对temp的修改,证明在这一步中temp的值是没有变化的,所以temp值不需要回溯
代码:
class Solution {
public:
vector<string> res;
void backtrack(string& S , string temp , vector<bool> &flags) {
if(S.size() == temp.size()) {
res.push_back(temp);
return;
}
for(int i = 0; i < S.size(); i++) {
if( (i > 0 && S[i-1] == S[i] && !flags[i - 1]) || flags[i]) continue; //重点
flags[i] = true;
backtrack(S , temp + S[i] , flags);
flags[i] = false;
}
}
vector<string> permutation(string S) {
vector<bool> flags(S.size() , false);
sort(S.begin() , S.end());
backtrack(S , "" , flags);
return res;
}
};
优化:
我们很容易发现这里存在很多重复性的遍历,在每一步中我们都要从第一个数开始重复的遍历,在使用标识位来判断是否已经使用过该数。
1.两两交换
我们不用temp作为递归下一步的参数,我们使用index。思路很简单,就是我们把第一个数和第二个数交换,然后回溯,再用第一个数和第三个数交换,再回溯。以此类推完成对全部数的交换。
class Solution {
public:
vector<string> res;
string temp;
void backtrack(string& S , int index , int sz) {
if(index >= sz) {
res.push_back(temp);
return;
}
for(int i = index; i < sz; i++) {
if( i > index && (S[i-1] == S[i] || S[i] == S[index])) continue;
temp += S[i];
swap(S[i] , S[index]);
backtrack(S , index + 1 , sz);
swap(S[i] , S[index]);
temp.pop_back();
}
}
vector<string> permutation(string S) {
int sz = S.size();
sort(S.begin() , S.end());
backtrack(S , 0 , sz);
return res;
}
};
这个方法有 点需要注意的:
- 剪枝条件:
- 两个数不能是同一个数
- 两个数不能是相等的数,这样交换就没有意义了
- 前一个数也是相同的数
- 选择列表是从index到size-1 , 不再是都是从 0 到 size - 1,这样即提高了效率,也避免重复
- 回溯操作,string删除最后一位,直接使用pop_back也是可以的
2.源函数法:
其实全列表有一个源函数直接表示,即next_permutation,返回的就是vector值
class Solution {
public:
vector<string> permutation(string S) {
vector<string> result;
sort(S.begin(), S.end());
result.push_back(S);
while (next_permutation(S.begin(), S.end())) {
result.push_back(S);
}
return result;
}
};
全排列 II
这题和上一题差不多的类型,所以就不在赘述前面的思路了,直接到代码,然后说下注意点
代码
class Solution {
public:
vector<vector<int>> res;
void backtrack(vector<int> nums , int index , int sz) { //难点1
if(index == sz - 1) {
res.push_back(nums);
return;
}
for(int i = index; i < sz; i++) {
if(i != index && nums[index] == nums[i]) continue;
swap(nums[i] , nums[index]); //难点2
backtrack(nums , index + 1 , sz);
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
int sz = nums.size();
sort(nums.begin() , nums.end());
backtrack(nums , 0 , sz);
return res;
}
};
总结:
难点1:因为当结束走到尽头回溯得回溯操作时,我们没有必要延续之前得操作,所以我们nums不需要引用,所以在难点2就不需要回溯步骤(这仅是个人理解,如有不对,请指点!)
复原IP地址
思路:
1.递归树
2.选择列表
就是对字符串进行分段然后判断,选择列表就是字符串
3.函数参数
这题有点特殊,虽然我们得选择列表是字符串,但是我们不使用字符串来遍历操作,而是使用分段得思想来遍历是否合理。所以我们得参数有:
s:字符串
n:合理字符串的个数
ip:合理得字符串
4.结束条件
当n == 4且s为空串时,储存并结束
5.每一步的逻辑
- 使用substr分段然后转化成int类型,设为val
- 判断val > 255 或者 val的字符串值是否首位有0的存在
6.剪枝
s串的数量不能小于i
7.递归下一步
s改为substr(i) ,n+1 ,ip + 合理字符串 + ‘.’ ,但是最后的一个ip值不需要加 . ,所以进行判断
8.回溯操作
无回溯操作,直接都在参数中完成
代码:
class Solution {
vector<string> res;
public:
vector<string> restoreIpAddresses(string s) {
string ip;
backtrack(s , 0 , ip);
return res;
}
void backtrack(string s , int n , string ip) {
if(n == 4) {
if (s.empty()) {
res.push_back(ip);
}
return;
}
for(int i = 1; i < 4; i++) {
if(s.size() < i) break;
int val = stoi(s.substr(0 , i));
if(val > 255 || i != to_string(val).size()) continue;
backtrack(s.substr(i) , n + 1 , ip + s.substr(0 , i) + (n == 3 ? "" : "."));
}
}
};
八皇后
思路:
这个8皇后的问题主要运用的是位运算方法,没有太多回溯法的思想。
这个图的思想,但第一行的Q在第二个的位置时,下一行的标识X的位置就不能摆放皇后,后面同理。
所以我们使用left,right,down三个数值来记录下一行不能放皇后的位置,然后使用 | 操作进行合并,这样就是下一行不能放皇后的全部位置。
left = ( (1 << i) | left ) << 1 :i 为皇后的位置。right ,down同理
还需要一个tmp容器来记录每一行皇后的位置,不用进行复值操作,最后在进行一个结果的储存时,在tmp的位置加上皇后即可
代码:
class Solution {
vector<vector<string>> res;
public:
vector<vector<string>> solveNQueens(int n) {
vector<int> tmp(n);
backtrack(n , tmp , 0 , 0 , 0 , 0);
return res;
}
void backtrack(int n , vector<int> &tmp , int left , int right , int down , int row) {
if(row == n) {
vector<string> ans(n, string(n,'.'));
for(int i = 0; i < n; i++) ans[i][tmp[i]] = 'Q';
res.push_back(ans);
return;
}
int isVaild = left | right | down;
for(int i = 0; i < n; i++) {
if(((1 << i) & isVaild) == 0) {
tmp[row] = i;
backtrack(n , tmp , ((1 << i) | left) << 1 , ((1 << i) | right) >> 1 , ((1 << i) | down) , row + 1);
}
}
}
};