教你做回溯题(Leetcode子集和子集 II)-第一期

本文介绍了回溯算法的概念,它是在深度优先遍历基础上,通过状态重置来寻找所有可能解的方法。文章通过子集问题的解题步骤详细阐述了如何使用回溯,包括确定结束条件、选择列表、剪枝等关键步骤,并提供了两种不同情况的代码实现:无重复元素的子集和包含重复元素的子集II。同时,强调了排序在剪枝中的作用。
摘要由CSDN通过智能技术生成

什么时候需要用到回溯,为什么回溯和递归有关?

在了解回溯之前,我们需要了解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();
        }
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值