leetcode-组合总和 II

201 阅读3分钟

这是我参与8月更文挑战的第29天,活动详情查看:8月更文挑战

周末总是过得特别快,转眼又到了周日的晚上。白天忙活了一顿午饭,请一些朋友来家里吃饭,现在才有空开电脑做做题,今天继续leetcode,第40题。

题目

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的每个数字在每个组合中只能使用一次。
注意:解集不能包含重复的组合。 

示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]

思路

跟昨天的题目其实非常类似,也是背包的变种,需要恰好装满的那种背包。今天跟昨天的题目,有2个重要的不同点:

  1. 每个数字只能使用一次
  2. 解集不能包含重复的组合 当然,需要注意的是,每个数字只能使用一次,不代表每个数字只能出现1次,因为在原数组中,就可能同一个数字出现多次的情况。 整体上,跟昨天的解法是非常类似的,但是有几个不一样的点:
  3. 因为每个数字只能使用一次,所以这个数字无论是否使用,递归进行深度优先搜索的时候,下一次就需要遍历i+1
  4. 解集不能包含重复的组合,所以出现1种解之后,需要跟之前的比较一下是否相同,进行去重 做到这2个点之后,其实已经可以求得正确的解,但是目前的效率还不够,提交后会出现TLE。那怎么加速呢?搜索的时候需要进行一些剪枝,除了昨天说的先对原数组排序,可以得到常数的优化外,还有一个比较重要的优化是,如果同一层循环中,遍历的candidates[i] = candidates[i-1],那么candidates[i]就一定不用被选择,这样可以进行一些剪枝,为什么?你可以这样理解,因为candidates[i] = candidates[i-1],如果选择candidates[i],那为什么不选择candidates[i-1]呢,因为candidates[i-1]被选择后,一定比candidates[i]被选择带来更多的组合,因为candidates[i+1]~candidates[len-1]一定是candidates[i]~candidates[len-1]的完全子集,所以选择candidates[i]组合的结果,一定已经在选择candidates[i-1]的时候已经选择过了。当然,candidates[i]和candidates[i-1]可以都选择,但是这种情况也是在candidates[i-1]的时候已经选择过了。 如图所示,蓝色区域是红色区域的完全子集:

40.png

Java版本代码

class Solution {
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        combinationSumList = new ArrayList<>();
        stack = new Stack<>();
        combinationSumDfs2(candidates, 0, target);
        return combinationSumList;
    }

    private static List<List<Integer>> combinationSumList = null;
    private static Stack<Integer> stack = null;

    private static void combinationSumDfs2(int[] candidates, int start, int target) {
        if (target == 0) {
            List<Integer> item = new ArrayList<>(stack);
            if (!combinationSumList.contains(item)) {
                combinationSumList.add(item);
            }
            return;
        }
        int len = candidates.length;
        if (start >= len) {
            return;
        }
        for (int i = start; i < len; i++) {
            // 同一层循环,相同的数值,第一个已经遍历过的情况下,第二个开始的可能组合数一定是第一次可能组合数的子集,所以结果肯定重复,可以直接剪枝
            if (i > start && candidates[i] == candidates[i - 1]) {
                continue;
            }
            if (candidates[i] <= target) {
                stack.push(candidates[i]);
                combinationSumDfs2(candidates, i + 1, target - candidates[i]);
                stack.pop();
            } else {
                break;
            }
        }
    }
}