LeetCode 《组合总和》系列题目总结(很nice!!!)

一、39.组合总和 I

在这里插入图片描述
思路:dfs,但是需要注意进行剪枝操作。

class Solution {
public:
    vector<vector<int>> v;
    vector<int > tmp,copy_candidates;
    int tg;
    void dfs(int u,int sum){
        if(u<0||sum > tg) return;
        if(sum == tg){
            v.push_back(tmp);
            return ;
        }
        tmp.push_back(copy_candidates[u]);
        dfs(u,sum+copy_candidates[u]);//可多次选取
        tmp.pop_back();
        dfs(u-1,sum);//不选了
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target){
        copy_candidates=candidates;
        tg=target;
        dfs(candidates.size()-1,0);//第一个数是正在遍历的数组下标,第二个数是总和
        return v;
        
    }
};

思考:这道题找的是组合数。
1.如果只返回组合数的个数,相当于完全背包问题,直接用dp做就可以。
2.这里需要返回所有可能的组合,那么我们就不能用dp去做,而是通过dfs,这样就可以保存所选的元素。
3.这里使用的dfs里,解决“同一个数字”可使用多次的地方是通过dfs(u,sum+copy_candidates[u])函数实现的,因为这里无重复元素的出现,所以可以放心大胆的用。但如果有重复的数字,那么我们就先对candidates数组进行一下去重操作即可。这样就避免了重复的组合。

二、 40.组合总和ll

在这里插入图片描述
思路:方法一,对数组candidates进行排序,然后对重复的数进行操作,把dfs里“重复的数”带来的“深度”变成“宽度”

class Solution {
public:
    vector<vector<int>> v;
    set<vector<int> > st;//解决重复的组合
    vector<int> tmp;
    vector<int> copy_candidates[105];//把重复的数,放进一个向量里,该向量只能选取其中一个数,向下遍历的深度更深
    int copy_target;
    int lens=0;
    void  dfs(int u,int sum){
        if(sum>copy_target) return;
        if(sum==copy_target){
            vector<int> t=tmp;
            sort(t.begin(),t.end());
            if(st.find(t)==st.end()){//去重操作
                v.push_back(t);
                st.insert(t);
            }
            return;
        }
        if(u==lens) return ;
        if(copy_candidates[u][0]+sum>copy_target) return;
        for(int i=0;i<copy_candidates[u].size();i++){//copy_candidates[u]向量里只能选一个
            for(int j=0;j<=i;j++)//这里是选的数
                tmp.push_back(copy_candidates[u][0]);
            dfs(u+1,sum+copy_candidates[u][i]);
            for(int j=0;j<=i;j++)
                tmp.pop_back();
        }
        dfs(u+1,sum);//不选copy_candidates[u]向量里的数
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(),candidates.end());//先排个序,好对重复的数进行处理
        int ct=1;
        for(int i=0;i<candidates.size();i++){
            if(i+1<candidates.size()&&candidates[i]==candidates[i+1]){
                copy_candidates[lens].push_back(ct*candidates[i]);
                ct++;
            }else{
                copy_candidates[lens++].push_back(ct*candidates[i]);
                ct=1;
            }
        }
        //sort(copy_candidates.begin(),copy_candidates.end());
        copy_target=target;
        dfs(0,0);
        return v;    
    }
};

思路:方法二,对数组candidates进行排序,然后使用dfs,在dfs里面我们用for循环来遍历接下来的u~candidates.size()-1。遍历的时候,如果前一个数copy_candidates[i-1]和当前的数copy_candidates[i]相同,那么我们就跳过。原因是在dfs“前一个数copy_candidates[i-1]”的时候,我们就已经考虑到了“当前的数copy_candidates[i]”是否使用,如果我们还dfs“当前的数copy_candidates[i]”,就相当于是重复了部分操作,会造成重复并且消耗大量的时间。这也是为什么使用for循环,他带来的好处就是可以避免“重复的数字”带来的“重复遍历”,影响结果和耗时。

class Solution {
public:

    vector<int> copy_candidates,tmp;
    vector<vector<int>> v;
    int copy_target;
    void dfs(int u,int sum){
        if(sum==copy_target){
            v.push_back(tmp);
            return ;
        }
        if(u==copy_candidates.size()||sum>copy_target){
            return;
        }
        for(int i=u;i<copy_candidates.size();i++){
            if(i==u||i>u&&copy_candidates[i]!=copy_candidates[i-1]){
                tmp.push_back(copy_candidates[i]);
                dfs(i+1,sum+copy_candidates[i]);
                tmp.pop_back();
            }
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(),candidates.end());
        copy_target=target;
        copy_candidates=candidates;
        dfs(0,0);
        return v;
    }
};

思考:方法一,就相当于已经处理了“重复的数字”带来的不同组合,把这些组合通过只能选一次的方法,来避免重复所带来的错误。也就相当于分组背包的问题。
方法二,利用for循环来巧妙的避免“重复的数字”带来的重复组合。

三、216.组合总和III

在这里插入图片描述
思路:就dfs,时间复杂度0(2^9)

class Solution {
public:
    vector<vector<int>> v;
    vector<int> tmp;
    void dfs(int u,int sum,int ct){
        if(sum<0) return ;
        if(sum==0&&ct==0){
            v.push_back(tmp);
            return ;
        }
        if(u>9||ct<=0) return;
        tmp.push_back(u);
        dfs(u+1,sum-u,ct-1);
        tmp.pop_back();
        dfs(u+1,sum,ct);

    }
    vector<vector<int>> combinationSum3(int k, int n) {
        dfs(1,n,k);
        return v;
    }
};

思考:每个数字最多使用一次,就相当于是二分了。

四、 377.组合总和IV

在这里插入图片描述

思路:方法一,深度搜索dfs+保存中间状态,即记忆化搜索,时间复杂度0(nums.length*target)。有个注意点:中间状态的存储数组f的初始化一定不能默认为0,会导致记忆化失败,最后超时。原因是因为部分中间状态sum它本身就是无法到达的,如果f默认为0的话,无法区别之前是否已经到达过这个sum,这样就会导致重复在这个无法到达的值sum深搜下去。
在每一个sum下都进行一次for循环,遍历所有的元素,也就是在sum这个位置上,选一个元素,这就相当于是排了个序,并且支持重复选择元素。
这里的for循环和40.组合总和ll 里的for循环内部存在不同,最后达成的效果也不同,这里是需要思考和注意的地方。

class Solution {
public:
    unsigned int f[1010]={1};
    vector<int> copy_nums;
    unsigned int  dfs(int sum){
        if(sum==0){
            return 1;
        }
        if(f[sum]!=-1) return f[sum];
        unsigned int ans=0;
        for(int i=0;i<copy_nums.size();i++){
            if(sum>=copy_nums[i]){
                ans+=dfs(sum-copy_nums[i]);
            }
        }
        return f[sum]=ans;
    }
    int combinationSum4(vector<int>& nums, int target) {
        copy_nums=nums;
        memset(f,-1,sizeof f);
        return dfs(target);
    }
};

思路:方法二,第一种方法是从上到下,通过状态数组来保存中间状态,避免大量的重复操作。现在第二种方法就是从下到上,通过动态规划dp就可以实现。所以dfs能做的题基本可以用dp。

class Solution {
public:
    unsigned  dp[1010];
    int combinationSum4(vector<int>& nums, int target) {
        dp[0]=1;
        for(int i=1;i<=target;i++){
            for(int j=0;j<nums.size();j++){
                if(nums[j]<=i) dp[i]+=dp[i-nums[j]];
            }
        }
        return dp[target];
    }
};

思考:1.顺序不同的组合视作不同的组合,说白了就是排列问题
2.如果dfs里sum是从sum=target开始往下减的,就是便于处理重复状态的问题,但缺陷就是一般只能计组合数,不能记住组合。
3.dp能做的题,通过“记忆化搜索=dfs+状态记忆”也能做。
4.记忆化搜索方法,一定要注意状态数组的初始化,尽量不要为0,因为无法判断之前是否到达过,从而弱化为普通的dfs,造成超时。
5.如果这里数组当中有重复的数,那我们还是通过排序后进行去重操作就可以了。因为每次for循环都是遍历整个数组,相当于每个数都有重复次机会,如果数组中有重复的数,那么就会产生重复的组合。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值