回溯法刷题入门总结

初看回溯时,觉得超级难以理解,也许是自己太笨了。后来看到大佬的总结,跟着他一步一步从基础题型入手,才慢慢掌握了一点点回溯法的技巧,这里做一个自己的学习总结,凡事还是得脚踏实地一步一步刷起。
如有错误恳请多多指正。
参考:「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 递增子序列

题目描述:
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

通信仿真爱好者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值