玩转 lee39 组合总和 (可重复、 lee40 组合总和2(不可重复)、lee216 组合总和3

14 篇文章 0 订阅
4 篇文章 0 订阅

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。

说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例 2:
输入: candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]

3.28 自己写的

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);
        List<List<Integer>> res=new ArrayList<>();
        helper(candidates, res, new ArrayList<>(), 0,target);
        return res;
    }
    private void helper(int[] num,List<List<Integer>> res, List<Integer> list, int start, int target){
        if(target<0) return;
        if(target==0){
            res.add(new ArrayList<>(list));
            return ;
        }
        for(int i=start;i<num.length;i++){
            if(num[i]>target) break;
            
            list.add(num[i]);
            target-=num[i];
            helper(num, res, list, i,target);
            list.remove(list.size()-1);
            target+=num[i];
        }
    }
}

回溯法 剪枝

解题思路:
做搜索、回溯问题的套路是画图,代码其实就是根据画出的树形图写出来的。
那么如何画图呢?
根据题目中的用例,画一个图,因为是搜索,因此呈现的是一个树形结构图,并且在这个树形结构中会体现出递归结构。
根据题目中的用例,比对自己画图的结果和题目的结果的差异,如果一样,说明我们的分析没有错;如果不一样,说明我们的分析有误,一定有哪一个环节漏掉了或者分析错误,根据找到的问题调整算法。

  1. 一个蓝色正方形表示的是 “尝试将这个数到数组 candidates 中找组合”,那么怎么找呢?挨个减掉那些数就可以了。

  2. 在减的过程中,会得到 0 和负数,也就是被我标红色和粉色的结点:

得到 0是我们喜欢的,从 0这一点向根结点走的路径(很可能只走过一条边,也算一个路径),就是一个组合,在这一点要做一次结算(把根结点到 0 所经过的路径,加入结果集)。

得到负数就说明这条路走不通,没有必要再走下去了。

在这里插入图片描述
根据回溯 剪枝 可写出简易版:

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> res=new ArrayList<>();
        if(candidates.length==0 || target<=0) return res;
        helper(candidates, target, 0, new ArrayList<>(), res);
        return res;
    }

    public void helper(int[] candidates, int target, int start,List<Integer> list, List<List<Integer>> res){
        if(target<0) return;
        if(target==0){
            res.add(new ArrayList<>(list));
            return ;
        }
        for(int i=start;i<candidates.length;i++){
            list.add(candidates[i]);
            helper(candidates , target-candidates[i], i, list, res);
            list.remove(list.size()-1);
        }
    }
}

画出图以后,我看了一下,我这张图画出的结果有 44 个 00,对应的路径是 [[2, 2, 3], [2, 3, 2], [3, 2, 2], [7]],而示例中的解集只有 [[7], [2, 2, 3]],很显然,我的分析出现了问题。问题是很显然的,我的结果集出现了重复。重复的原因是

后面分支的更深层的边出现了前面分支低层的边的值。

限于我的表达能力有限,大伙意会这句话就可以了,看一看重复的叶子结点 00 的路径,想一想重复的原因,或许你会比我说得更清楚更好。

但是这个问题也不难解决,把候选数组排个序就好了(想一下,结果数组排个序是不是也可以去重),后面选取的数不能比前面选的数还要小,即 “更深层的边上的数值不能比它上层的边上的数值小”,按照这种策略,剪枝就可以去掉重复的组合。

在这里插入图片描述
补充:事实上,不排序也是可以的,只要保证按顺序读取,也可以通过测试用例,我个人还是觉得排序更好一些,这样“剪枝”工作可以更彻底一些。

优化后代码:

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> res=new ArrayList<>();
        if(candidates.length==0 || target<=0) return res;
         // 优化添加的代码1:先对数组排序,可以提前终止判断
        Arrays.sort(candidates);
        helper(candidates, target, 0, new ArrayList<>(), res);
        return res;
    }

    public void helper(int[] candidates, int target, int start,List<Integer> list, List<List<Integer>> res){
        if(target<0) return;
        if(target==0){
            res.add(new ArrayList<>(list));
            return ;
        }
        // 优化添加的代码2:在循环的时候做判断,尽量避免系统栈的深度
        // residue - candidates[i] 表示下一轮的剩余,如果下一轮的剩余都小于 0 ,就没有必要进行后面的循环了
        // 这一点基于原始数组是排序数组的前提,因为如果计算后面的剩余,只会越来越小
        for(int i=start;i<candidates.length && target-candidates[i]>=0;i++){
            list.add(candidates[i]);
            // 【关键】因为元素可以重复使用,这里递归传递下去的是 i 而不是 i + 1
            helper(candidates , target-candidates[i], i, list, res);
            list.remove(list.size()-1);
        }
    }
}

lee40 组合总和2

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。

说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
与上一题的区别:
就是 不可重复
在递归时,递归函数参数 变成 i+1

在判断是否重复时怎么办?

在for循环递归中 剪枝

if(target==0 && !res.contains(list)){
            res.add(new ArrayList<>(list));
            return ;
        }

这样还是比较费事

还可以这样
这个方法最重要的作用是
可以让同一层级,不出现相同的元素。但是却允许了不同层级之间的重复
为何会有这种神奇的效果呢?
首先 cur-1 == cur 是用于判定当前元素是否和之前元素相同的语句。这个语句就能砍掉例1。可是问题来了,如果把所有当前与之前一个元素相同的都砍掉,那么例二的情况也会消失。 因为当第二个2出现的时候,他就和前一个2相同了。

那么如何保留例2呢?
那么就用cur > begin 来避免这种情况,你发现例1中的两个2是处在同一个层级上的,
例2的两个2是处在不同层级上的。在一个for循环中,所有被遍历到的数都是属于一个层级的 ,我们要让一个层级中,必须出现且只出现一个2,那么就放过第一个出现重复的2,但不放过后面出现的2。第一个出现的2的特点就是 cur == begin. 第二个出现的2 特点是cur > begin.

在这里插入图片描述

class Solution {
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<List<Integer>> res=new ArrayList<>();
        if(candidates.length==0 || target<=0) return res;
         // 优化添加的代码1:先对数组排序,可以提前终止判断
        Arrays.sort(candidates);
        helper(candidates, target, 0, new ArrayList<>(), res);
        return res;
    }

    public void helper(int[] candidates, int target, int start,List<Integer> list, List<List<Integer>> res){
        if(target<0) return;
        if(target==0 ){
            res.add(new ArrayList<>(list));
            return ;
        }
        // 优化添加的代码2:在循环的时候做判断,尽量避免系统栈的深度
        // residue - candidates[i] 表示下一轮的剩余,如果下一轮的剩余都小于 0 ,就没有必要进行后面的循环了
        // 这一点基于原始数组是排序数组的前提,因为如果计算后面的剩余,只会越来越小
        for(int i=start;i<candidates.length && target-candidates[i]>=0;i++){
        if(i>start && candidates[i-1]==candidates[i]){
			continue;
        }
            list.add(candidates[i]);
            // 【关键】因为元素可以重复使用,这里递归传递下去的是 i 而不是 i + 1
            helper(candidates , target-candidates[i], i+1, list, res);
            list.remove(list.size()-1);
        }
    }
}

这样立马性能提升很多
!!!嘿嘿

3.28自己写的

class Solution {
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        List<List<Integer>> res=new ArrayList<>();
        helper(candidates, res, new ArrayList<>(), 0,target);
        return res;
    }
    private void helper(int[] num,List<List<Integer>> res, List<Integer> list, int start, int target){
        if(target<0) return;
        if(target==0){
            res.add(new ArrayList<>(list));
            return ;
        }
        for(int i=start;i<num.length;i++){
            if(num[i]>target) break;
            if(i>start && num[i]==num[i-1]) continue;
            list.add(num[i]);
            target-=num[i];
            helper(num, res, list, i+1,target);
            list.remove(list.size()-1);
            target+=num[i];
        }
    }
}

lee216 组合总和3

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

说明:
所有数字都是正整数。
解集不能包含重复的组合。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]

3.28 自己写的

class Solution {
    public List<List<Integer>> combinationSum3(int k, int n) {
        List<List<Integer>> res=new ArrayList<>();
        if(n<=0 || k<=0 ) return res;
        helper(k,n,res, new ArrayList<>(), 1);
        return res;
    }
    private void helper(int k, int n, List<List<Integer>> res, List<Integer> list, int start){
        if(n<0) return ;
        if(list.size()==k && n==0 ){
            res.add(new ArrayList<>(list));
            return ;
        }
        for(int i=start;i<=9;i++){
            list.add(i);
            helper(k,n-i,res, list, i+1);
            list.remove(list.size()-1);
        }
    }
}    

class Solution {
    public List<List<Integer>> combinationSum3(int k, int n) {
        List<List<Integer>> res=new ArrayList<>();
        if(k<=0 || k>9 || n<=0) return res;
        helper(k , n, res , new ArrayList<Integer>(),1);
        return res;
    }

    public void helper(int k, int n ,List<List<Integer>> res,List<Integer>  list, int start){
        if(n<0) return;
        if(n==0 && list.size()==k){
            res.add(new ArrayList<>(list));
            return;
        }
        for(int i=start;i<=9 ;i++){
            if(n-i<0) continue;//加上这句 性能提高到70%
            list.add(i);
            helper(k, n-i ,res, list, i+1);
            list.remove(list.size()-1);
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值