回溯题目总结 1


前言

今天主要做了回溯的经典例题,特此总结一下。

一、全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

这道题思路比较简单,只需要将所有的分支遍历一遍即可,只需要注意用过的数不可以再用。

class Solution {
public:
    vector<vector<int>> permute(vector<int>& nums) {
        vector<int> used;
        vector<int> path;
        vector<vector<int>> res;
        for(int i=0;i<nums.size();i++)  used.push_back(0);//均未使用
        if(nums.size()==0)  return res;
        dfs(nums,used,path,res,nums.size());
        return res;
        
    }
    void dfs(vector<int>& nums,vector<int> &used,vector<int> &path,vector<vector<int>> &res,int len)
    {
        if(path.size()==len)  {res.push_back(path);return ;}
        for(int i=0;i<nums.size();i++)
        {
            if(!used[i])//用过的数不可以再使用
            {
                path.push_back(nums[i]);
                used[i]=1;
                dfs(nums,used,path,res,len);
                used[i]=0;//从一个分支转移到另一个时要把之前用的消除掉
                path.pop_back();
            }
        }
    }
};

二、全排列 II

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
这道题与上一道不同的是,数据重复,但是结果不能重复,同样是全排列。
这里就需要考虑如何剪枝,假如仍然使用上一题的思路,会出现重复。
由于结果和顺序有关,因而不同于组合问题,第一个分支能够将所有包含该分支顶端数的组合全包含。但是假如一个数和前一个数相同,且前一个数未被使用,即在同一层的话,那么这两个数就是等价的,可以将后面的一支删掉。

class Solution {
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<int> used;
        vector<int> path;
        vector<vector<int>> res;
        sort(nums.begin(),nums.end());
        for(int i=0;i<nums.size();i++)  used.push_back(0);//均未使用
        if(nums.size()==0)  return res;
        dfs(nums,used,path,res,nums.size());
        return res;
        
    }
    void dfs(vector<int>& nums,vector<int> &used,vector<int> &path,vector<vector<int>> &res,int len)
    {
        if(path.size()==len)  {res.push_back(path);return ;}
        for(int i=0;i<nums.size();i++)
        {
            if(!used[i])
            {
                if(i>=1&&nums[i]==nums[i-1]&&!used[i-1]) continue;//回溯到同一层时,如果之前用的和目前的数一样,且之前的数未使用,那么状态相同,直接剪枝
                else 
                {
                    path.push_back(nums[i]);
                    used[i]=1;
                    dfs(nums,used,path,res,len);
                    used[i]=0;
                    path.pop_back();
                }
            }
        }
    }
};

三、组合总和

和全排列不同,这道题里每个数都可以被无限次选取,且结果与顺序无关。所以,我们需要考虑到,假如每次都从0开始遍历,势必出现重复的元素,但是第0个元素其对应的分支,就能够涵盖所有包含第0个元素的组合,同理,第一次遍历到第一个数对应的分支,也能涵盖所有出现了第一个数的组合,因而之后再进行遍历时,遍历的起点应为当前的循环变量i。(为i表示下一层仍可以使用该数,即无限次选取)

class Solution {
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<vector<int>> res;
        vector<int> path;
        sort(candidates.begin(),candidates.end());
        if(candidates.size()==0)  return res;
        dfs(candidates,target,path,res,0);
        sort(res.begin(),res.end());
        return res;
    }
    void dfs(vector<int>& candidates,int value,vector<int> &path,vector<vector<int>> &res,int begin)
    {
        
        if(value<0) return;
        if(value==0)
        {
            res.push_back(path);
            return ;
        }
        for(int i=begin;i<candidates.size();i++)
        {
            if(target-candidates[i]<0)  break;
            if(value-candidates[i]>=0) 
            {
                path.push_back(candidates[i]);
                dfs(candidates,value-candidates[i],path,res,i);
                path.pop_back();
            }
        }
    }
};

四、组合总和 II

和上一道题不同,这道题要求每个数只能出现一次,且会出现重复元素。假如仍使用上一道题的方式,那么同一层的重复元素会导致出现重复的结果,只需要保留第一个相同值的分支即可。每个数只出现一次,通过下一层的下标从i+1开始来实现。

class Solution {
public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        vector<int> used;
        for(int i=0;i<candidates.size();i++) used.push_back(0);
        vector<int> path;
        vector<vector<int>>  res;
        if(candidates.size()==0)  return res;
        dfs(candidates,target,path,res,0,used);
        return res;
    }
    void dfs(vector<int>& candidates, int leftval,vector<int> &path,vector<vector<int>> &res,int begin,vector<int> & used)
    {
        if(leftval<0)  return ;
        if(leftval==0) {res.push_back(path); return ;}
        for(int i=begin;i<candidates.size();i++)
        {
            if(i > begin && candidates[i]==candidates[i-1])  continue;//这里加不加前一个数是否被使用都没影响,因为我们是按次序遍历,在这之前前一个分支已经退出了,一定未被使用
            if(leftval-candidates[i]<0)  break;
            if(!used[i])
            {
                used[i]=1;
                path.push_back(candidates[i]);
                dfs(candidates,leftval-candidates[i],path,res,i+1,used);//注意这里使用的是i,不是begin,因为剪枝过程中,前一个数已经找到了所有包含其的组合,后一个数不需要再找包含前一个数的组合;从同一个数往下面依次寻找结果时,放在前面的一定找到了所有包含该数的组合,后面的就不能使用该数;used只能用来排除同意分支上同一层的元素,不能排除不同分支同层,而用i+1则可以完全杜绝掉后面的值又使用前面元素的情况。
                path.pop_back();
                used[i]=0;
            }
        }
    }

};

总结

从以上题目来看,涉及到路径的问题,剪枝的方式往往是对重复的一层操作,具体实现上是used使用以及判断和前一个数是否相等。而对于涉及组合的类型,由于先被遍历到的数,已经涵盖了所有包含该数的种类,后面再出现该数会出现重复,因而实现方式是每次的遍历起点都要增加,从而禁止后面的组合使用前面出现的数字,同时也要注意,需要预先对数组排序,才能进一步剪枝,对同一个数的重复分支,只保留第一个即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值