Java轻松搞定leetcode前100系列之40. 组合总和 II

题目描述:

给定一个数组 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]
]


示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
  [1,2,2],
  [5]
]

回溯算法+剪枝

解题思路:

一句话题解:按顺序搜索,设置合理的变量,在搜索的过程中判断是否会出现重复集结果。重点理解对输入数组排序的作用和 参考代码 中大剪枝和小剪枝 的意思。

与第 39 题(组合之和)的差别

这道题与上一问的区别在于:

第 39 题:candidates 中的数字可以无限制重复被选取;
第 40 题:candidates 中的每个数字在每个组合中只能使用一次。
相同点是:相同数字列表的不同排列视为一个结果。

如何去掉重复的集合(重点)

为了使得解集不包含重复的组合。有以下 22 种方案:

使用 哈希表 天然的去重功能,但是编码相对复杂;
这里我们使用和第 39 题和第 15 题(三数之和)类似的思路:不重复就需要按 顺序 搜索, 在搜索的过程中检测分支是否会出现重复结果 。注意:这里的顺序不仅仅指数组 candidates 有序,还指按照一定顺序搜索结果。

由第 39 题我们知道,数组 candidates 有序,也是 深度优先遍历 过程中实现「剪枝」的前提。
将数组先排序的思路来自于这个问题:去掉一个数组中重复的元素。很容易想到的方案是:先对数组 升序 排序,重复的元素一定不是排好序以后相同的连续数组区域的第 11 个元素。也就是说,剪枝发生在:同一层数值相同的结点第 2、3 ... 个结点,因为数值相同的第 1 个结点已经搜索出了包含了这个数值的全部结果,同一层的其它结点,候选数的个数更少,搜索出的结果一定不会比第 11 个结点更多,并且是第 11 个结点的子集。(说明:这段文字很拗口,大家可以结合具体例子,在纸上写写画画进行理解。)

对于如何去重还不太清楚的朋友,下面有一个解释:

解释语句: if cur > begin and candidates[cur-1] == candidates[cur] 是如何避免重复的。

这个避免重复当思想是在是太重要了。
这个方法最重要的作用是,可以让同一层级,不出现相同的元素。即
                  1
                 / \
                2   2  这种情况不会发生 但是却允许了不同层级之间的重复即:
               /     \
              5       5
                例2
                  1
                 /
                2      这种情况确是允许的
               /
              2  
                
为何会有这种神奇的效果呢?
首先 cur-1 == cur 是用于判定当前元素是否和之前元素相同的语句。这个语句就能砍掉例1。
可是问题来了,如果把所有当前与之前一个元素相同的都砍掉,那么例二的情况也会消失。 
因为当第二个2出现的时候,他就和前一个2相同了。
                
那么如何保留例2呢?
那么就用cur > begin 来避免这种情况,你发现例1中的两个2是处在同一个层级上的,
例2的两个2是处在不同层级上的。
在一个for循环中,所有被遍历到的数都是属于一个层级的。我们要让一个层级中,
必须出现且只出现一个2,那么就放过第一个出现重复的2,但不放过后面出现的2。
第一个出现的2的特点就是 cur == begin. 第二个出现的2 特点是cur > begin.

代码如下:

    ArrayList<List<Integer>> res = new ArrayList<>();
    List<Integer> combines = new ArrayList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        //关键步骤
        Arrays.sort(candidates);
        
        dfs(candidates,0,target);
        return res;
    }

    private void dfs(int[] candidates, int begin, int target) {
        //定义结束条件
        if (target==0){
            res.add(new ArrayList<>(combines));
            return;
        }
        for (int i = begin; i < candidates.length; i++) {
            //大剪枝,如果当前的target-candidates[i]<0了,那么target-candidates[i+1]、target-candidates[i+2] 肯定也小于0,因此用break,直接剪掉
            if (target-candidates[i]<0){
                break;
            }
            //小剪枝,同一层相同数值的结点,从第2个开始,候选数更少,结果一定发生重复,因此跳过,用continue
            if (i>begin&&candidates[i]==candidates[i-1]){
                continue;
            }
            combines.add(candidates[i]);
            // 调试语句 ①
            //System.out.println("递归之前 => " + combines + ",剩余 = " + (target - candidates[i]));

            dfs(candidates,i+1,target-candidates[i]);
            //回溯
            combines.remove(combines.size()-1);
            //调试语句2
            //System.out.println("递归之后 => " + combines + ",剩余 = " + (target + candidates[i]));
        }
    }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值