组合总和 II
力扣原题链接
问题描述
给定一个候选人编号的集合 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
每个数字在每个组合中只能使用一次。
注意:解集不能包含重复的组合。
示例
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]![请添加图片描述](https://img-blog.csdnimg.cn/direct/d78f91f8af2f4df2968d937666d1e8d5.jpeg)
示例 2:
输入: candidates = [2,5,2,1,2], target = 5
输出:
[
[1,2,2],
[5]
]
解题思路
这个问题与前面的组合总和问题类似,但是每个数字在每个组合中只能使用一次。为了避免重复,我们需要在递归搜索的过程中进行剪枝,避免选取相同的数字。
- 排序数组: 首先对候选人编号的集合
candidates
进行排序,以便后续剪枝操作。 - 回溯搜索: 定义回溯函数
backtrack
,其参数包括当前处理的索引start
、当前的目标数target
、当前的数字和sum
、当前的组合路径path
和记录数字是否使用过的数组use
。 - 结束条件: 如果当前数字和等于目标数,将当前组合路径加入结果列表,并返回。
- 选择列表: 获取当前可选的数字集合。
- 遍历选择: 遍历当前可选的数字集合,对每个数字进行递归搜索。
- 剪枝操作: 在递归搜索的过程中,如果当前数字等于上一个数字且上一个数字未被使用过,则跳过当前数字,避免选取重复的组合。
- 做出选择: 将当前数字加入路径,并将当前数字标记为已使用。
- 递归进入下一层: 递归调用回溯函数,传入新的索引
i + 1
、新的目标数target - candidates[i]
、新的数字和sum + candidates[i]
、新的组合路径path
和更新后的记录数组use
。 - 撤销选择: 回溯到上一层时,将当前选择的数字从路径中删除,并将当前数字的使用状态标记为未使用。
Java解题
写法一
import java.util.*;
class Solution {
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<Integer> path = new ArrayList<>();
boolean[] use = new boolean[candidates.length];
Arrays.fill(use, false);
Arrays.sort(candidates); // 排序数组
backtrack(candidates, target, 0, 0, path, use);
return res;
}
public void backtrack(int[] candidates, int target, int start, int sum, List<Integer> path, boolean[] use) {
if (sum == target) { // 结束条件
res.add(new ArrayList<>(path));
return;
}
for (int i = start; i < candidates.length; i++) {
if (sum > target) break; // 剪枝
if (i > 0 && candidates[i] == candidates[i - 1] && !use[i - 1]) {
continue; // 剪枝
}
path.add(candidates[i]); // 做出选择
use[i] = true;
backtrack(candidates, target, i + 1, sum + candidates[i], path, use); // 递归进入下一层
use[i] = false; // 撤销选择
path.remove(path.size() - 1);
}
}
}
写法二
class Solution {
List<List<Integer>> combinations = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates); // 排序数组
backtrack(candidates, target, 0, new ArrayList<>());
return combinations;
}
private void backtrack(int[] candidates, int target, int start, List<Integer> path) {
if (target == 0) {
combinations.add(new ArrayList<>(path));
return;
}
for (int i = start; i < candidates.length; i++) {
// 剪枝:如果当前数字等于上一个数字且当前索引不等于起始索引,则跳过当前数字
if (i > start && candidates[i] == candidates[i - 1]) {
continue;
}
if (target - candidates[i] < 0) {
// 剪枝:如果当前数字大于目标数,则跳过,因为后续数字只会更大
continue;
}
// 做出选择
path.add(candidates[i]);
// 递归进入下一层
backtrack(candidates, target - candidates[i], i + 1, path);
// 撤销选择
path.remove(path.size() - 1);
}
}
}