题目描述:
给定一个数组 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]));
}
}