8.组合总数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]
]
- 题目分析
相较于之前的组合总数问题,本题会出现重复组合的问题,比如说candidates=[1,2,7,6,1,5] target = 8时
以1为第一个元素的时候,组合中会出现满足条件的[1,2,5]
以2为第一个元素的时候,组合中会出现满足条件的[2, 1, 5]
不难发现这两个是重复的,因此需要去重。
为什么会出现这种情况?
这是因为当第一个元素取1的时候就已经包含了[1,2,5]这组集合元素,再2为第一个元素时就会导致重复,而且我们发现当且只有在同一数层之上会出现这一种情况,那么如何避免呢?
这里可以通过标记的方法解决这一问题
创建used元素用于记录元素的使用情况
i > 0 && candidates[i] == candidates[i - 1] && used[i] == 1 && used[i - 1] == 0
i > 0: 确保当前遍历的数字不是数组的第一个元素,因为第一个元素无法和前一个元素比较。
candidates[i] == candidates[i - 1]: 检查当前数字是否和前一个数字相同,如果相同则表示存在重复的元素。
used[i] == 1 && used[i - 1] == 0: 确保当前数字已经被使用过,而前一个相同的数字没有被使用过。这个条件是为了避免重复的组合。
综合起来,这个条件的含义是:如果当前数字和前一个数字相同,并且当前数字已经被使用,而前一个数字没有被使用,则应该跳过当前数字,以避免生成重复的组合。
- Java代码实现
LinkedList<Integer> path = new LinkedList<>(); // 用于存储当前组合的路径
List<List<Integer>> result = new ArrayList<>(); // 用于存储最终结果的列表
/**
* 寻找给定候选数组中和为目标值的所有不重复组合
* @param candidates 候选数组
* @param target 目标值
* @return 所有符合条件的组合列表
*/
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
int[] used = new int[candidates.length]; // 用于标记候选数组中数字是否被使用过
Arrays.sort(candidates); // 对候选数组进行排序
backtrack(candidates, used, 0, target, 0); // 调用回溯函数开始搜索
return result; // 返回最终结果
}
/**
* 回溯函数,用于搜索符合条件的组合
* @param candidates 候选数组
* @param used 标记数组
* @param startIndex 当前搜索的起始位置
* @param target 目标值
* @param sum 当前累计和
*/
private void backtrack(int[] candidates, int[] used, int startIndex, int target, int sum) {
if (sum > target) return; // 如果当前累计和已经超过目标值,则返回
if (sum == target) { // 如果当前累计和等于目标值
result.add(new ArrayList<>(path)); // 将当前路径加入最终结果
return;
}
for (int i = startIndex; i < candidates.length; i++) {
used[i] = 1; // 标记当前数字为已使用
if (i > 0 && candidates[i] == candidates[i - 1] && used[i] == 1 && used[i - 1] == 0) {
used[i] = 0; // 如果出现重复数字且没有按顺序使用,则跳过当前数字
continue;
}
path.add(candidates[i]); // 将当前数字加入路径
sum += candidates[i]; // 更新累计和
backtrack(candidates, used, i + 1, target, sum); // 递归调用下一层搜索
used[i] = 0; // 恢复当前数字为未使用
path.removeLast(); // 移除当前数字
sum -= candidates[i]; // 恢复累计和
}
}
- 不用used数组标记的做法
1.在 combinationSum2 方法中,去除了不必要的参数 used,因为在优化后的方法中并未使用到该参数。同时,对 startIndex 和 sum 也进行了调整,这两个参数被通过 path 和 result 的状态来推导,避免了传递过多参数。
2.在 backtrack 方法中,剪枝操作的位置进行了调整。将重复元素的剪枝操作放在了循环体内的开头,这样可以在进入递归之前就剔除掉重复的情况,减少了不必要的递归次数,提高了效率。
3.循环的起始位置进行了调整,并增加了对重复元素的判断。在每次循环时,如果当前元素与前一个元素相同,则直接跳过,避免了重复计算,进一步提高了效率。
4.将 sum 的计算放在了循环体内,使得每次递归只需要计算当前元素的值,而不需要重复计算之前元素的值。
核心修改
if (i > startIndex && candidates[i] == candidates[i - 1]) {
continue; // 如果当前数字和前一个数字相同,则跳过当前数字,避免重复组合
}
解释:在回溯过程中,候选数组已经经过排序,如果当前数字和前一个数字相同,那么意味着在上一层的递归中已经考虑过这个数字,为了避免生成相同的组合,我们需要跳过当前数字。
i > startIndex:这个条件确保我们只考虑从当前位置开始的数字,而不是之前已经被考虑过的数字。
candidates[i] == candidates[i - 1]:这个条件判断当前数字是否和前一个数字相同。
continue:如果当前数字和前一个数字相同,我们使用continue语句跳过当前数字的处理,直接进入下一次循环。
- Java代码实现
LinkedList<Integer> path = new LinkedList<>(); // 用于存储当前组合的路径
List<List<Integer>> result = new ArrayList<>(); // 用于存储最终结果的列表
/**
* 寻找给定候选数组中和为目标值的所有不重复组合
* @param candidates 候选数组
* @param target 目标值
* @return 所有符合条件的组合列表
*/
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates); // 对候选数组进行排序
backtrack(candidates, 0, target); // 调用回溯函数开始搜索
return result; // 返回最终结果
}
/**
* 回溯函数,用于搜索符合条件的组合
* @param candidates 候选数组
* @param startIndex 当前搜索的起始位置
* @param target 目标值
*/
private void backtrack(int[] candidates, int startIndex, int target) {
if (target == 0) { // 如果目标值为0,表示当前组合符合条件
result.add(new ArrayList<>(path)); // 将当前路径加入最终结果
return;
}
for (int i = startIndex; i < candidates.length; i++) {
if (i > startIndex && candidates[i] == candidates[i - 1]) {
continue; // 如果当前数字和前一个数字相同,则跳过当前数字,避免重复组合
}
int num = candidates[i]; // 获取当前数字
if (num > target) {
break; // 如果当前数字已经大于目标值,结束循环
}
path.add(num); // 将当前数字加入路径
backtrack(candidates, i + 1, target - num); // 递归调用下一层搜索
path.removeLast(); // 移除当前数字
}
}