彻底理解力扣《组合总和II》题目,深刻理解如何去重!
① 题目
给定一个候选人编号的集合 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]
]
提示:
-
1 <= candidates.length <= 100
-
1 <= candidates[i] <= 50
-
1 <= target <= 30
② 整体代码
public class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
// 回溯法
// 回溯的参数:candidates,target,startIndex, result,path
// 回溯的返回值:void
// 回溯的终止条件:pathSum > target,说明不可能通过加一个正数达到 target。我们采用 target == 0 进行判断
// 单层回溯的逻辑:横向遍历处理 candidates 中剩下的每一个元素
// 纵向遍历就是从 candidates 中拿出元素继续进行相加
// ① candidates[i] == candidates[i - 1] && !used[i - 1] 表示同一层使用过该数字
// ② candidates[i] == candidates[i - 1] && used[i - 1] 表示同一层使用过该数字!
// 加入 boolean[] used 数组让我们很好的将数值和数层分开了,只要递归进去那么前一个的boolean一定是true,只要是for循环
// 进行的横向遍历,那么前一个的boolean一定因为回溯变为了false!
if (target == 0) {
return Collections.emptyList();
}
Arrays.sort(candidates);
used = new boolean[candidates.length];
Arrays.fill(used, false);
backtarcking(candidates, target, 0);
return result;
}
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new ArrayList<>();
boolean[] used;
public void backtarcking(int[] candidates, int target, int startIndex) {
if (target == 0) {
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i < candidates.length; i++) {
int newtarget = target - candidates[i];
// 排序 + 剪枝
if (newtarget < 0) {
break;
}
// 数层去重
if (i > 0 && candidates[i] == candidates[i - 1] && !used[i - 1]) {
System.out.println("同一数层 使用过该数字:" + candidates[i]);
continue;
}
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1]) {
System.out.println("同一树枝 使用过该数字:" + candidates[i]);
}
// 处理当前索引元素
used[i] = true;
path.add(candidates[i]);
// 回溯搜索,i + 1 就是进行树枝去重
backtarcking(candidates, newtarget, i + 1);
// 回溯当前索引元素
used[i] = false;
path.remove(path.size() - 1);
}
}
}
③ 思路
参考“代码随想录”的思路进行理解,我们要清楚理解 树枝去重 和 树层去重
树枝去重:
树枝去重很好理解,一条从根节点到叶子节点的路径,不同重复使用“同一个元素”(这里的同一个元素指的candidates数组中同一个位置的元素),对于这部分的去重思路就是每次递归的时候将数组的开始索引向后加一即可。例如树层1能够选取的元素是0到末尾,那么树层2能够选取的元素就是1到末尾。
// 回溯搜索,i + 1 就是进行树枝去重
backtarcking(candidates, newtarget, i + 1);
树层去重:
首先我们要理解为什么需要进行树层去重?
例如数组 candidates = [1, 1, 2, 5, 6, 7, 10] ,target = 8。
那么从索引0开始进行递归搜索,会找到 [1,2,5] 的结果集,对应数组的索引是 0,2,3
从索引1开始进行递归搜索,也会找到 [1,2,5] 的结果集!对应数组的索引是 1,2,3
这就是为什么我们需要进行数层去重!
那下面就是本文章主要解决的问题,如何进行数层去重?
我们是不是很容易想到添加通过下面的if判断进行去重,但是这样的话我们就找不到 [1,1,6] 这种结果集了。问题的原因是不是因为我们无法区分当前是 树枝元素重复 还是 树层元素 重复?
if( i > 0 && candidates[i] == candidates[i - 1] )
我们引入 boolean[] used 的数组进行辅助确认 candidates 中数组元素的使用情况。
for (int i = startIndex; i < candidates.length; i++) {
// 处理当前索引元素
used[i] = true;
path.add(candidates[i]);
// 回溯搜索,i + 1 就是进行树枝去重
backtarcking(candidates, newtarget, i + 1);
// 回溯当前索引元素
used[i] = false;
path.remove(path.size() - 1);
}
初始时 used 数组默认全为 false,只要回溯搜索的过程中,处理了当前索引的元素,那么就将 used 修改为 true。同时进行回溯的时候将 used 修改为 fasle!
情况一: 处理当前索引元素之后,我们进入了递归,那么是不是就代表了当前处理的是同一个树的树枝,
在树枝的角度观看candidates[i-1],即前一个元素,used[i-1]是不是为 true !
// 处理当前索引元素
used[i] = true;
path.add(candidates[i]);
// 回溯搜索,i + 1 就是进行树枝去重
backtarcking(candidates, newtarget, i + 1);
**情况二:**处理当前索引元素之后,我们进入了递归。递归结束之后(也就是一条分支结束搜索),我们需要进行回溯当前元素,也就是会将used[i]修改为false。然后进入下一个for循环(就代表要“开辟”一个新的树枝),所以当前就是站在同一树层的角度观看candidates[i-1],即前一个元素,used[i-1]是不是为 false!
for (int i = startIndex; i < candidates.length; i++) {
// 处理当前索引元素
used[i] = true;
path.add(candidates[i]);
// 回溯搜索,i + 1 就是进行树枝去重
backtarcking(candidates, newtarget, i + 1);
// 回溯当前索引元素
used[i] = false;
path.remove(path.size() - 1);
}
**总结:**我们就是利用树枝和数层观看前一节点,会有不同的结果进行去重的。
// 满足该条件的代表:同一树枝 使用过该数字,对于同一树枝,数字的重复是被允许的
i > 0 && candidates[i] == candidates[i - 1] && used[i-1]
// 满足该条件的代表:同一树层 使用过该数字,对于同一树枝,数字的重复是不被允许的
i > 0 && candidates[i] == candidates[i - 1] && !used[i-1]