首页 简历|笔试面试

40.组合总和II

  • 25年9月4日 发布
  • 1011.48KB 共6页
40.组合总和II40.组合总和II40.组合总和II40.组合总和II40.组合总和II

40. 组合总和 II

二哥说,今天去帮父亲干了半天的活,一身尘埃,但是感觉很充实,累但知道了

生活的不易,更需要去珍惜现在拥有的一切,所以马不停蹄地继续刷题,今天的

题目是组合总和 II,这道题目是一道中等难度的题目,但是,只要我们细心分

析,就能迎刃而解。

题意

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有

可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次 。

注意:解集不能包含重复的组合。

难度

中等

示例

输入: candidates = [10,1,2,7,6,1,5], target = 8,

输出:

[

[1,1,6],

[1,2,5],

[1,7],

[2,6]

]

输入: candidates = [2,5,2,1,2], target = 5,

输出:

[

[1,2,2],

[5]

]

分析 1

很显然,这道题和 039.组合总和 是姊妹篇。不过,这道题要求同一个数字不能被重复选

择,但是 candidates 中可能会存在有重复的数字。

同样我们也可以使用暴力,枚举所有的可能性,然后判断是否符合条件,如果符合条件就

加入到答案中。

class Solution04001 {

public List<List<Integer>> combinationSum2(int[] candidates, int target) {

List<List<Integer>> result = new ArrayList<>();

Arrays.sort(candidates); // 排序以便于后续处理

generateAllCombinations(candidates, target, new ArrayList<>(), result, 0);

return result;

}

private void generateAllCombinations(int[] candidates, int target,

List<Integer> combination, List<List<Integer>> result, int start) {

int sum = combination.stream().mapToInt(Integer::intValue).sum();

if (sum > target) {

return;

}

if (sum == target) {

if (!result.contains(combination)) {

result.add(new ArrayList<>(combination)); // 创建组合的副本并添加到结果

列表

}

return;

}

for (int i = start; i < candidates.length; i++) {

combination.add(candidates[i]);

generateAllCombinations(candidates, target, combination, result, i + 1); //

每个数字只能使用一次,所以递归从 i+1 开始

combination.remove(combination.size() - 1); // 回溯,撤销最后的选择

}

}

public static void main(String[] args) {

Solution04001 solution = new Solution04001();

int[] candidates = {10, 1, 2, 7, 6, 1, 5};

int target = 8;

List<List<Integer>> result = solution.combinationSum2(candidates, target);

System.out.println("所有组合: " + result);

}

}

代码很好理解:

• 计算当前组合的和 sum。

• 如果 sum 大于 target,直接返回,进行剪枝操作。

• 如果 sum 等于 target,检查结果列表中是否已经包含该组合,若不包含则添加该组

合。

• 循环遍历从 start 到 candidates.length 的每一个元素:

• 将当前元素 candidates[i] 加入组合列表 combination。

• 递归调用 generateAllCombinations,继续生成可能的组合。

• 回溯,撤销当前选择,继续尝试下一个元素。

直接运行 main 方法也是可以得出结果的。

040.组合总和 II-20240804185133.png

但是很遗憾,放在 LeetCode 上跑的时候,会发现超时了。

040.组合总和 II-20240804185205.png

问题就在于,重复计算太多,和 039.组合总和 一样,我们需要优化。

分析 2

优化的思路和 039.组合总和 是一样的,我们可以先对数组进行排序,然后在递归的时

候:

• 如果当前数字和上一个数字相同,我们直接跳过。

• 如果当前候选数字大于剩余的目标值,我们直接返回,然后进行剪枝操作。比如说

[1, 1, 2, 5, 6, 7],目标值是 8,如果当前数字是 5,那么剩余的目标值是 4,而 5 已

经大于 4 了,所以直接返回。然后把 2 剪枝掉,因为 1+1+2+后面任何一个数都不满

足要求了。

好,我们来看题解:

class Solution04002 {

private List<List<Integer>> result = new ArrayList<>();

public List<List<Integer>> combinationSum2(int[] candidates, int target) {

Arrays.sort(candidates); // 排序以便于跳过重复元素

backtrack(candidates, target, 0, new ArrayList<>());

return result;

}

private void backtrack(int[] candidates, int target, int start, List<Integer>

combination) {

if (target == 0) {

result.add(new ArrayList<>(combination));

return;

}

for (int i = start; i < candidates.length; i++) {

// 跳过同一层中重复的元素

if (i > start && candidates[i] == candidates[i - 1]) {

continue;

}

// 剪枝:当前候选数大于剩余目标值,直接返回

if (candidates[i] > target) {

break;

}

combination.add(candidates[i]);

// 递归调用时,下一次递归的起点要从当前元素的下一个元素开始

backtrack(candidates, target - candidates[i], i + 1, combination);

combination.remove(combination.size() - 1); // 回溯,撤销最后的选择

}

}

public static void main(String[] args) {

Solution04002 solution = new Solution04002();

int[] candidates = {10, 1, 2, 7, 6, 1, 5};

int target = 8;

List<List<Integer>> result = solution.combinationSum2(candidates, target);

System.out.println("所有组合: " + result);

}

}

基本情况:如果 target == 0,说明找到了一个符合条件的组合,将其加入结果列表

result。

循环遍历从 start 到 candidates.length 的每一个元素:

• 如果当前元素与前一个元素相同,则跳过,以避免重复组合(排序过了)。

• 如果当前元素大于 target,直接剪枝,跳出循环。

• 否则,将当前元素加入组合列表 combination,递归调用 backtrack,判断这组数字

是否符合条件。

• 如果不符合,回溯,撤销当前选择,继续尝试下一个元素。

运行效率也很不错。直接击败了 99.87% 的选手。

040.组合总和 II-20240804190003.png

这其中的关键代码有五处。

第一处,对数组进行排序,以便于后续跳过重复元素:

Arrays.sort(candidates); // 排序以便于跳过重复元素

第二处,跳过同一层中重复的元素:

if (i > start && candidates[i] == candidates[i - 1]) {

continue;

}

比如说 [1,1,2,2] 这种,如果 target 是 4,那么第二个 2 的时候,就不需要再递归了。

结果就只有 [1,1,2] 和 [2,2]。

第三处,剪枝操作,当前候选数大于剩余目标值,直接返回:

if (candidates[i] > target) {

break;

}

比如说 [1, 1, 2, 5, 6, 7],目标值是 8,如果当前数字是 5,那么剩余的目标值是 4,而 5

已经大于 4 了,所以直接返回。

第四处,递归调用时,下一次递归的起点要从当前元素的下一个元素开始:

backtrack(candidates, target - candidates[i], i + 1, combination);

比如说 [1, 1, 2, 5, 6, 7],目标值是 8,如果之前数字是 1+1+2,那么下一次递归的起点

应该是 5,也就是从 2 的下一个元素开始。

第五处,回溯,撤销最后的选择:

combination.remove(combination.size() - 1); // 回溯,撤销最后的选择

比如说 [1, 1, 2, 5, 6, 7],目标值是 8,如果当前数字是 5,前面是 1+1+2,返回后需要从

1+1 开始尝试,撤销 2 这个选择。从 1+1+5 开始尝试。

总结

这道题和 039.组合总和 是姊妹篇,但要求不同,上一个是可以重复选择一个数,这次是

有重复元素,但是同一个数字只能使用一次。

本质上还是一个回溯的问题,但需要跳过同一层的重复元素。

力扣链接:https://leetcode.cn/problems/combination-sum-ii/

开通会员 本次下载免费

所有资料全部免费下载! 推荐用户付费下载获取返佣积分! 积分可以兑换商品!
一键复制 下载文档 联系客服