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(); // 移除当前数字}
}