问题
给定一个数组 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]
]
思路
这道题目和leetcode-39:求组合总和如下区别:
- 本题
candidates
中的每个数字在每个组合中只能使用一次 - 本题数组
candidates
的元素是有重复的,而之前是无重复元素的数组candidates
最后本题要求解集不能包含重复的组合
本题的难点在于:集合(数组candidates)有重复元素,但还不能有重复的组合
我们需要在搜索的过程中就去掉重复组合
所谓去重,其实就是使用过的元素不能重复选取
都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因
那么问题来了,我们是要同一树层上使用过,还是同一树枝上使用过呢?
回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同
所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重
为了理解去重我们来举一个例子,candidates = [1, 1, 2], target = 3,(方便起见candidates已经排序了)
选择过程树形结构如图所示:
可以看到图中,每个节点相对于leetcode-39:求组合总和多加了used
数组
回溯三部曲
递归函数参数
- 此题还需要加一个
bool
型数组used
,用来记录同一树枝上的元素是否使用过List<List<Integer>> result = new ArrayList<List<Integer>>(); // 存放组合集合
List<Integer> path = new ArrayList<Integer>(); // 符合条件的组合
public void backtracking(int[] candidates, int target, int sum, int startIndex, boolean[] used)
- 此题还需要加一个
递归终止条件
if (sum > target) { // 这个条件其实可以省略
return;
}
if (sum == target) {
result.add(new ArrayList<Integer>(path));
return;
}
单层搜索的逻辑
- 这里最需要注意的地方就是去重
- 前面我们提到:要去重的是“同一树层上的使用过”,如何判断同一树层上元素(相同的元素)是否使用过了呢
- 这里最需要注意的地方就是去重
如果**candidates[i] == candidates[i - 1]**
并且 **used[i - 1] == false**
,就说明:前一个树枝,使用了**candidates[i - 1]**
,也就是说同一树层使用过**candidates[i - 1]**
此时for循环里就应该做continue
的操作
这块比较抽象,如图:
我在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]
相同的情况下:
used[i - 1] == true
,说明同一树支candidates[i - 1]
使用过used[i - 1] == false
,说明同一树层candidates[i - 1]
使用过for (int i = startIndex; i < candidates.length && sum + candidates[i] <= target; i++) {
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
continue;
}
sum += candidates[i];
path.add(candidates[i]);
used[i] = true;
backtracking(candidates, target, sum, i + 1, used);
used[i] = false;
sum -= candidates[i];
path.remove(path.size() - 1);
}
class Solution {
List<List<Integer>> result = new ArrayList<List<Integer>>(); // 存放组合集合
List<Integer> path = new ArrayList<Integer>(); // 符合条件的组合
public void backtracking(int[] candidates, int target, int sum, int startIndex, boolean[] used){
if (sum == target) {
result.add(new ArrayList<Integer>(path));
//Java默认传引用,将path变量中的元素放入result中,需要重新new一个
return;
}
for (int i = startIndex; i < candidates.length && sum + candidates[i] <= target; i++) {
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
continue;
}
sum += candidates[i];
path.add(candidates[i]);
used[i] = true;
backtracking(candidates, target, sum, i + 1, used);
used[i] = false;
sum -= candidates[i];
path.remove(path.size() - 1);
}
}
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
boolean[] used = new boolean[candidates.length];
// 首先把给candidates排序,让其相同的元素都挨在一起。
Arrays.sort(candidates);
backtracking(candidates, target, 0, 0, used);
return result;
}
}
官解
```java import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Deque; import java.util.List;
public class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
int len = candidates.length;
List<List<Integer>> res = new ArrayList<>();
if (len == 0) {
return res;
}
// 关键步骤
Arrays.sort(candidates);
Deque<Integer> path = new ArrayDeque<>(len);
dfs(candidates, len, 0, target, path, res);
return res;
}
/**
* @param candidates 候选数组
* @param len 冗余变量
* @param begin 从候选数组的 begin 位置开始搜索
* @param target 表示剩余,这个值一开始等于 target,基于题目中说明的"所有数字(包括目标数)都是正整数"这个条件
* @param path 从根结点到叶子结点的路径
* @param res
*/
private void dfs(int[] candidates, int len, int begin, int target, Deque<Integer> path, List<List<Integer>> res) {
if (target == 0) {
res.add(new ArrayList<>(path));
return;
}
for (int i = begin; i < len; i++) {
// 大剪枝:减去 candidates[i] 小于 0,减去后面的 candidates[i + 1]、
// candidates[i + 2] 肯定也小于 0,因此用 break
if (target - candidates[i] < 0) {
break;
}
// 小剪枝:同一层相同数值的结点,从第 2 个开始,候选数更少,结果一定发生重复,
// 因此跳过,用 continue
if (i > begin && candidates[i] == candidates[i - 1]) {
continue;
}
path.addLast(candidates[i]);
// 调试语句 ①
// System.out.println("递归之前 => " + path + ",剩余 = " +
// (target - candidates[i]));
// 因为元素不可以重复使用,这里递归传递下去的是 i + 1 而不是 i
dfs(candidates, len, i + 1, target - candidates[i], path, res);
path.removeLast();
// 调试语句 ②
// System.out.println("递归之后 => " + path + ",剩余 = " +
// (target - candidates[i]));
}
}
public static void main(String[] args) {
int[] candidates = new int[]{10, 1, 2, 7, 6, 1, 5};
int target = 8;
Solution solution = new Solution();
List<List<Integer>> res = solution.combinationSum2(candidates, target);
System.out.println("输出 => " + res);
}
}
递归之前 => [1],剩余 = 7 递归之前 => [1, 1],剩余 = 6 递归之前 => [1, 1, 2],剩余 = 4 递归之后 => [1, 1],剩余 = 4 递归之前 => [1, 1, 5],剩余 = 1 递归之后 => [1, 1],剩余 = 1 递归之前 => [1, 1, 6],剩余 = 0 递归之后 => [1, 1],剩余 = 0 递归之后 => [1],剩余 = 6 递归之前 => [1, 2],剩余 = 5 递归之前 => [1, 2, 5],剩余 = 0 递归之后 => [1, 2],剩余 = 0 递归之后 => [1],剩余 = 5 递归之前 => [1, 5],剩余 = 2 递归之后 => [1],剩余 = 2 递归之前 => [1, 6],剩余 = 1 递归之后 => [1],剩余 = 1 递归之前 => [1, 7],剩余 = 0 递归之后 => [1],剩余 = 0 递归之后 => [],剩余 = 7 递归之前 => [2],剩余 = 6 递归之前 => [2, 5],剩余 = 1 递归之后 => [2],剩余 = 1 递归之前 => [2, 6],剩余 = 0 递归之后 => [2],剩余 = 0 递归之后 => [],剩余 = 6 递归之前 => [5],剩余 = 3 递归之后 => [],剩余 = 3 递归之前 => [6],剩余 = 2 递归之后 => [],剩余 = 2 递归之前 => [7],剩余 = 1 递归之后 => [],剩余 = 1 输出 => [[1, 1, 6], [1, 2, 5], [1, 7], [2, 6]] ```