2203 字
约 7 分钟
6
040.组合总和 II
无标签

040.组合总和 II

作者:沉默王二
发布时间:2024-08-04T19:24:57.139+0800


二哥说,今天去帮父亲干了半天的活,一身尘埃,但是感觉很充实,累但知道了生活的不易,更需要去珍惜现在拥有的一切,所以马不停蹄地继续刷题,今天的题目是组合总和 II,这道题目是一道中等难度的题目,但是,只要我们细心分析,就能迎刃而解。

题意

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

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

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

难度

中等

示例

textjavascripttypescriptcsshtmlbashjsonmarkdownpythonjavaccpprubygorustphpsqlyaml Copy
输入: 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中可能会存在有重复的数字。

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

textjavascripttypescriptcsshtmlbashjsonmarkdownpythonjavaccpprubygorustphpsqlyaml Copy
class Solution04001 {
public List<List> combinationSum2(int[] candidates, int target) {
List<List> 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 方法也是可以得出结果的。

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

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

分析 2

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

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

如果当前候选数字大于剩余的目标值,我们直接返回,然后进行剪枝操作。比如说 [1, 1, 2, 5, 6, 7],目标值是 8,如果当前数字是 5,那么剩余的目标值是 4,而 5 已经大于 4 了,所以直接返回。然后把 2 剪枝掉,因为 1+1+2+后面任何一个数都不满足要求了。

好,我们来看题解:

textjavascripttypescriptcsshtmlbashjsonmarkdownpythonjavaccpprubygorustphpsqlyaml Copy
class Solution04002 {
private List<List> 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% 的选手。

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

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

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

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

textjavascripttypescriptcsshtmlbashjsonmarkdownpythonjavaccpprubygorustphpsqlyaml Copy
if (i > start && candidates[i] == candidates[i - 1]) {
continue;
}

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

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

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

textjavascripttypescriptcsshtmlbashjsonmarkdownpythonjavaccpprubygorustphpsqlyaml Copy
if (candidates[i] > target) {
break;
}

比如说 [1, 1, 2, 5, 6, 7],目标值是 8,如果当前数字是 5,那么剩余的目标值是 4,而 5 已经大于 4 了,所以直接返回。

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

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

比如说 [1, 1, 2, 5, 6, 7],目标值是 8,如果之前数字是 1+1+2,那么下一次递归的起点应该是 5,也就是从 2 的下一个元素开始。

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

textjavascripttypescriptcsshtmlbashjsonmarkdownpythonjavaccpprubygorustphpsqlyaml Copy
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/

一步一个脚印

不积跬步无以至千里,不积小流无以成江海。LeetCode - 100天从算法小白到卷王正式启动了,我们计划周一到周五至少每天更新一篇,周六周日更新一篇,目标 300 道 LeetCode 经典题。

PDF 版本目前只针对二哥编程星球 的球友开放,如果你也想一起打卡学习算法的话,戳链接 加入我们吧。

一个人可以走得很快,但一群人才能走得更远。二哥编程星球 (目前已有 5900 多名球友加入)里面的每个球友都非常的友善,除了鼓励你,还会给你提出合理的建议,一起冲!

语雀专栏地址+密码可通过星球置顶帖查看:https://t.zsxq.com/15rEo9Pdu

这道题可以说完全重写了,给出 1 道大家完全都能掌握的题解( 主要是理解递归)。

增加代码注释,这样大家更容易看懂并掌握题解;

增加参考资料,这样大家可以从零基础直接开刷,减轻新手刷题的阻力;

增加手绘图,这样大家更容易理解解题思路;

增加 ACM 输入输出,这样大家可以在 Intellij IDEA 中进行调试;

增加题解模拟,这样大家可以根据题解模拟整个题解过程,看完会有一种“哦哦”恍然大悟的感觉;

图片上传至 OSS 并配合 CDN,提升大家在技术派网站中的阅读体感。

另外,这一版本会作为 PDF 的终稿随后在星球进行发布。

技术派阅读地址:https://paicoding.com/column/7/40

040.组合总和 II
https://clxhxhhr.top/posts/2465/
作者
clxstart
发布于
2026-09-19
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。
☰ 文章目录