什么时候需要用到回溯,为什么回溯和递归有关?
在了解回溯之前,我们需要了解dfs(dfs总结指路),dfs就是深度优先遍历,简单来说,dfs就是一口气往往某个深度方向搜索,而回溯是建立在dfs的基础之上的,在搜索时,达到结束条件后,需要恢复之前改变的状态,再次搜索。
①就是说比dfs多了一个状态重置,当题目需要在找到一组解或一组解不符合要求后回头来找到所有的解,这时候就需要使用回溯了。通俗来说,就是当走通了或走不通时,要撤销选择,回退到上一个状态继续尝试其他走法,直到找出所有可走通的走法。
②因为深度优先遍历一般是使用递归实现,回溯是建立在其基础之上的,所以涉及到递归。
回溯题解题步骤
①选择一个实际例子,画出递归树(多画图真的对于递归的流程会清晰很多,尤其是新手,通过树形帮助理解递归执行过程很有帮助)。
②确定结束条件(即一条路走通结束的条件,而回溯题一般需要找出所有解)
③确定选择的列表,这步很重要,这个列表可以理解为我们画的树的每一层的选择(到时候结合图更好分析)。
④是否需要剪枝(这个算难点,因题而异,主要用于去重的,有时候需要多找几个实例多画几个树才能找出去重条件)。
⑤选择
⑥递归调用,进入下一层(这里的的下一层对于树来说是往深处(竖向的),而选择列表是横向的)
⑦撤销(有选择必有撤销,这是回溯题的特色)
例题
做题前一般固定需要的参数
vector<vector<int>> res;//结果集
vector<int> path;//可行结果
子集
给一个互不相同的元素集合,返回其子集(包含空集与本身)
①选择一个实际例子集合{1,2,3},画图
②确定结束条件
由于这题是求子集,上图中所有节点得到的[]值都需要加入结果集 。所以可以说没有结束条件
直接
res.push_back(path);//将可行结果假如结果集
③确定选择列表
我们标出图各层的选择列表,就是找出各个子树,其子节点构成的层就是选择列表
我们发现选择列表结尾都是3,也就是题目所给数集合的最后一位,起点是当前层的节点得到的结果的下一位,也就是说是上一条选择路径之后的数。
for(int i=begin;i<nums.size();i++) //begin表示上一条路径的下一位
④没有重复,无需剪枝
⑤⑥⑦选择、递归、撤销
//选择
path.push_back(nums[i]);
//递归
dfs(nums,res,path,i+1);
//撤销
path.pop_back();
完整代码:
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> res;
vector<int> path;
dfs(nums,res,path,0);
return res;
}
void dfs(vector<int>& nums,vector<vector<int>>& res,vector<int>& path,int begin){
//这题求子集,所有路径都应该加入结果集,所以不存在结束条件
res.push_back(path);
for(int i=begin;i<nums.size();i++){
//选择
path.push_back(nums[i]);
//递归
dfs(nums,res,path,i+1);
//撤销
path.pop_back();
}
}
};
子集 II
在子集的基础上对于给定的数组集合可能包含重复元素,那么如果还用子集的方式解决,是肯定有重复的,所以需要剪枝。
①选择一个实际例子[1,2,2]画图
②③步与题目子集一样,但是这题节点中的结果明显有重复的,所以需要剪枝。
④剪枝去重
图上标记的节点结果重复了。比如这题因为2==2导致有两处出现重复,需要减去右边的分枝
也就是说此时的选择列表不能遍历重复的,比如原来的选择列表是[1,2,2],现在只能是[1,2],要去掉重复的2,选择列表为[2,2],只能为2,去掉重复的2。也就是说当nums[i-1]==nums[i]时此时i应该去掉因为重复了,直接跳过该次循环。
if(i>begin && nums[i-1]==nums[i]){ //边界判断 && 重复判断
continue; //跳过循环
}
不过通过nums[i-1]==nums[i]来检查重复的前提是数组已经排序好了,因为有序数组中相同的数才能挨在一起
//排序数组,为剪枝做好准备
sort(nums.begin(),nums.end());
⑤⑥⑦选择、递归、撤销和子集那题一样
完整代码:
class Solution {
public:
//在子集一的基础上去除当前选择列表中,与上一个数重复的那个数引出的分支
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<vector<int>> res;
vector<int> path;
//排序数组,为剪枝做好准备
sort(nums.begin(),nums.end());
dfs(nums,res,path,0);
return res;
}
void dfs(vector<int>& nums,vector<vector<int>>& res,vector<int>& path,int begin){
//这题求子集,所以都可
res.push_back(path);
for(int i=begin;i<nums.size();i++){
//剪枝去重
//在当前选择列表中,去除与前面一个数重复的数引出的分支(通过continue直接跳过)
if(i>begin && nums[i-1]==nums[i]){
continue;
}
//选择
path.push_back(nums[i]);
//递归
dfs(nums,res,path,i+1);
//撤销
path.pop_back();
}
}
};