初看回溯时,觉得超级难以理解,也许是自己太笨了。后来看到大佬的总结,跟着他一步一步从基础题型入手,才慢慢掌握了一点点回溯法的技巧,这里做一个自己的学习总结,凡事还是得脚踏实地一步一步刷起。
如有错误恳请多多指正。
参考:「leetcode」最强回溯算法总结篇!历时21天、画了20张树形结构图、14道精选回溯题目精讲
文章目录
1.回溯法基本框架
关于回溯法的伪代码,leecode及百度上有很多文章总结过,以c++的vector为例如下
定义 result 数组
定义 path 数组
void backtrack(路径, 选择列表){
if 满⾜结束条件:
{ result.push_back(路径)
return;
}
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
path.pop_back()//撤销选择
}
初看上述结构,觉得很难以理解,可以继续看看下图,以我们高中常见的排列题目为例子,列举数字1,2,3 的全排列,我一般是采用这样的树形结构去举例的,借用labuladong书上的一张图:
以图中紫色圈节点为例,我们在画图列举的时候,因为之前已经选择了1,所以选择列表里还剩下2,3。到达紫色节点的时候,我们继续进行下一步选择,我们可以先选2,为图中红色箭头,那么选完2之后选择列表里只剩下3了。我们发现还可以选择3,这是我们需要把2放回选择列表中,然后先选择3,那么选择列表里只剩下2了。
相信大家的穷举过程和上述过程大同小异,其实我们在不知不觉间就用到了这个回溯法,只是当时的我们还不知道这个专有名词。
可以看到,当我们从紫色圈节点进入下一个节点时,类似于二叉树的深度优先搜索dfs,同样可以采用递归操作完成。蓝色箭头代表回溯过程,只是在dfs的基础上加了回溯且修改当前节点的状态的操作。
看到这,对于回溯法伪代码的理解会不会深一点点。
看到这其实我还有点懵,道理其实简单,但是翻译成代码就不知道何从下手,结合伪代码,配上几个例子,对回溯的理解会深很多。
2.涉及题目
2.1组合问题:
可以按照这个顺序刷下去
1.leetcode 77 组合
2.leetcode 39 组合总和_1
3.leetcode 40 组合总和_2
4.leetcode 216 组合总和_3
5.leetcode 377 组合总和_4
1)77组合
题目描述:给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
如上图所示,假如第一次取1后,选择列表变为[2,3,4],因为k=2,剩下只要在其中取一个数就行了。由此可以得到框架中的递归结束条件。又因为假设一次结果为[1,2] ,另一次选择结果为[2,1],这两者实际上是重复的,因此我们需要对选择列表的范围随着选择的进行来收缩。即采用一个参数int index,记录递归的起始位置。for循环横向遍历的时候,保证取过的数不会再取。
代码如下:
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combine(int n, int k) {
dfs(n,k,1);
return res;
}
void dfs(int n,int k,int index)
{
if(path.size()==k) //结束条件
{
res.push_back(path);
return;
}
for(int i=index;i<=n;i++)
{
path.push_back(i);
dfs(n,k,i+1); //回溯
path.pop_back();//撤销选择
}
}
};
上述代码还有优化的空间,就是更改for循环里的循环条件,具体可参考这篇文章:回溯算法:组合问题再剪剪枝
for(int i=index;i<=n-(k-path.size())+1;i++)
有点类似于鸡兔同笼问题优化暴力穷举的思想。
2)39组合总和1
题目描述:给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
注意:这里candidates 里的数字可被无限重复选取,而candidates里的元素无重复。
这里的递归中止条件为sum=target。其余的情况大同小异,
因为元素可被无限重复选取,因此递归时传入的参数仍为i,意思为可以重复读取当前的数,套用回溯框架,可以写出如程序。
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
int sum;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
dfs(candidates,target,0,sum);
return res;
}
void dfs(vector<int>& candidates, int target, int index,int sum)
{
if(sum==target)
{
res.push_back(path);
return;
}
if(sum>target)
return;
for(int i=index;i<candidates.size();i++)
{
sum+=candidates[i];
path.push_back(candidates[i]);
dfs(candidates,target,i,sum);//不用i+1了,表示可以重复读取当前的数
sum-=candidates[i];
path.pop_back();
}
}
};
同样对于for循环的部分可进行剪枝优化,对于上图可看到,上述代码,对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回。
其实,如果已经知道下一层的sum会大于target,就没有必要进入下一层递归了。
如下图:
对总集合先排序后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历
优化代码为:
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
int sum;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end()); //先排序
dfs(candidates,target,0,sum);
return res;
}
void dfs(vector<int>& candidates, int target, int index,int sum)
{
if(sum==target)
{
res.push_back(path);
return;
}
if(sum>target)
return;
for(int i=index;i<candidates.size()&&sum+candidates[i]<=target;i++)
{
sum+=candidates[i];
path.push_back(candidates[i]);
dfs(candidates,target,i,sum);
sum-=candidates[i];
path.pop_back();
}
}
};
3)leetcode 40 组合总和_2
**题目描述:**给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
注意:1.candidates 中的每个数字在每个组合中只能使用一次。2.candidates里的元素可能重复。
这题相比于39题,难点在于去除最后得到重复的结果。至于每个元素只能使用一次这个条件,我们修改递归里的index即可。
一种可想到的思路是,利用hasp或set去重,代码如下
注意,这里需要先对candidates排序,不然会出现
上述原因在于, 给出输入用例中,1,2,5 和 2,1,5 被认为是不同的set中的元素。
所以我们只需将数组排序后变为 1,1,2,5,6,7,10, 那么在set中,会有 1,2,5 和1,2,5 被认为是相同的,去重了。
正确代码:使用set
class Solution {
public:
vector<int> path;
set<vector<int>> res;
int sum;
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());
dfs(candidates,target,0,sum);
return vector<vector<int>>(res.begin(),res.end()); //类型转换
}
void dfs(vector<int>& candidates, int target, int index,int sum)
{
if(sum==target)
{
res.insert(path);
return;
}
if(sum>target)
return;
for(int i=index;i<candidates.size();i++)
{
sum+=candidates[i];
path.push_back(candidates[i]);
dfs(candidates,target,i+1,sum);
sum-=candidates[i];
path.pop_back();
}
}
};
另一种思路,先对candidates排序,使得相同元素聚焦在一起,在递归时,如果发现2个相邻元素相同,则可以直接跳过了,相应代码if(i>index&&candidates[i]==candidates[i-1])
,i>index含义为,当index为0时,i需要大于0,i-1才有含义。
注意,这里必须这么写:if(i>index&&candidates[i]==candidates[i-1])
先后顺序不能颠倒,否则 i-1 不存在 ,只有先保证i>index 才能使得i-1存在。
颠倒了会报错。
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
int sum;
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());
dfs(candidates,target,0,sum);
return res;
}
void dfs(vector<int>& candidates, int target, int index,int sum)
{
if(sum==target)
{
res.push_back(path);
return;
}
if(sum>target)
return;
for(int i=index;i<candidates.size();i++)
{
if(i>index&&candidates[i]==candidates[i-1])
continue;
sum+=candidates[i];
path.push_back(candidates[i]);
dfs(candidates,target,i+1,sum);//
sum-=candidates[i];
path.pop_back();
}
}
};
3)216 组合总和3
题目描述:找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
分析一下,这个题目的选择列表是固定的了,为1-9,且可选择的元素默认没有重复的;选择的元素不重复,递归中止条件相比于前面2道多了一个。写起来也比较简单了,如下:
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
int sum=0;
vector<vector<int>> combinationSum3(int k, int n) {
dfs(k,n,1,sum);
return res;
}
void dfs(int k, int n,int index,int sum)
{
if(path.size()==k)
{ if(sum==n)
{
res.push_back(path);
return;
}
return;
}
for(int i=index;i<=9;i++)
{
sum+=i;
path.push_back(i);
dfs(k,n,i+1,sum);
path.pop_back();
sum-=i;
}
}
};
2.2 子集问题
1.leetcode 78 子集
2.leetcode 90 子集2
3.leetcode491 递增子序列
1) 78 子集
如图所示,与子集问题的区别就在于,组合问题是在叶子节点处得到一组结果,而子集问题是收集所有节点的结果。
递归中止条件为 index的值大于nums.size,可不写。代码如下
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> subsets(vector<int>& nums) {
dfs(nums,0);
return res;
}
void dfs(vector<int>& nums, int index)
{
res.push_back(path);
for(int i=index;i<nums.size();i++)
{
path.push_back(nums[i]);
dfs(nums,i+1);
path.pop_back();
}
}
};
2) 90 子集2
题目描述:给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
这题相对于上一题,变成了数组元素重复,而结果集不能重复
一组思路是用set去重,类比于前面写过的 leetcode 40 组合问题中的set去重,代码如下:
class Solution {
public:
set<vector<int>> res;
vector<int> path;
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(),nums.end());
dfs(nums,0);
return vector<vector<int>>(res.begin(),res.end());
}
void dfs(vector<int>& nums, int index)
{
res.insert(path);
for(int i=index;i<nums.size();i++)
{
path.push_back(nums[i]);
dfs(nums,i+1);
path.pop_back();
}
}
};
另一种思路,先对nums排序,使得相同元素聚焦在一起,在递归时,如果发现2个相邻元素相同,则可以直接跳过了。代码如下
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(),nums.end());
dfs(nums,0);
return res;
}
void dfs(vector<int>& nums, int index)
{
res.push_back(path);
for(int i=index;i<nums.size();i++)
{
if(i>index&&nums[i]==nums[i-1])
continue;
path.push_back(nums[i]);
dfs(nums,i+1);
path.pop_back();
}
}
};
3)491 递增子序列
题目描述: