哈喽各位,我是前端小L。
欢迎来到我们的回溯算法专题第五篇!今天我们要解决的,是组合问题中约束最复杂的一种情况。
我们要在一个乱序、有重复的数组中,找出和为 target 的组合,且不能有重复解。
这道题就像是在告诉我们:"既要(数字凑够),又要(不能重复用),还要(解集不重复)"。
面对这种"既要又要还要"的需求,我们的回溯模板需要开启"完全体"模式:排序 + 树层去重 + 剪枝。
力扣 40. 组合总和 II
https://leetcode.cn/problems/combination-sum-ii/

题目分析:
-
输入 :数组
candidates(有重复),目标target。 -
目标 :找出所有和为
target的组合。 -
约束:
-
每个数字在每个组合中只能使用一次。
-
解集不能包含重复的组合。
-
例子:
candidates = [10,1,2,7,6,1,5], target = 8
-
排序后:
[1, 1, 2, 5, 6, 7, 10] -
组合
[1, 7](第一个1) -
组合
[1, 7](第二个1) -> 重复!必须去掉! -
组合
[1, 2, 5] -
组合
[2, 6] -
组合
[1, 1, 6]
核心策略:排序 + "树层去重"
这道题的去重逻辑,和 LC 90 子集 II 完全一致!
- 必须排序
为了让重复的元素聚在一起,方便我们判断,第一步必须是 sort。
2. 树层去重 vs 树枝去重
-
树枝去重(纵向):同一个分支深处,能不能选重复的元素?
- 题目说"每个数字只能用一次",指的是同一个索引 的数字只能用一次。但如果数组里有两个
1(索引不同),我们当然可以在一个组合里同时选这两个1(比如[1, 1, 6])。这是允许的。
- 题目说"每个数字只能用一次",指的是同一个索引 的数字只能用一次。但如果数组里有两个
-
树层去重(横向):同一个父节点下,能不能选值相同的兄弟?
-
假如第一层选了第一个
1,我们会递归搜索出所有以1开头的组合(包括[1, 7])。 -
回溯后,同层循环到了第二个
1。如果我们再选它,必然会生成重复的[1, 7]。这是禁止的。
-
通用去重模板:
C++
if (i > startIndex && candidates[i] == candidates[i-1]) {
continue;
}
i > startIndex:确保是"同层后续元素"(横向),而不是"递归下一层元素"(纵向)。
- 递归参数
因为每个数字只能用一次,所以递归时传入 i + 1(指向下一个数),而不是 i。
代码实现 (C++)
C++
#include <vector>
#include <algorithm> // for sort
using namespace std;
class Solution {
private:
vector<vector<int>> res;
vector<int> path;
int currentSum = 0;
void backtrack(vector<int>& candidates, int target, int startIndex) {
// 1. 结束条件 (成功)
if (currentSum == target) {
res.push_back(path);
return;
}
// 2. 遍历选择列表
for (int i = startIndex; i < candidates.size(); ++i) {
// --- 核心剪枝 1:数值剪枝 ---
// 因为数组已排序,如果加上当前数这就超了,后面的肯定更超
// 直接 break,大幅提升效率
if (currentSum + candidates[i] > target) {
break;
}
// --- 核心剪枝 2:树层去重 ---
// 如果当前元素和前一个元素相同,且前一个元素没被选(i > startIndex)
// 说明是同层重复,跳过
if (i > startIndex && candidates[i] == candidates[i-1]) {
continue;
}
// 做选择
path.push_back(candidates[i]);
currentSum += candidates[i];
// 递归
// 关键:传入 i + 1,因为当前元素不可复用
backtrack(candidates, target, i + 1);
// 撤销选择
currentSum -= candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
res.clear();
path.clear();
currentSum = 0;
// 0. 必须排序!去重和剪枝的基础
sort(candidates.begin(), candidates.end());
backtrack(candidates, target, 0);
return res;
}
};
深度对比:组合问题的"三剑客"
到现在,我们已经彻底拿下了 LeetCode 上最经典的三道"组合"题。让我们最后一次横向对比,把它们刻在脑子里:
| 题目 | LC 39 组合总和 | LC 40 组合总和 II | LC 216 组合总和 III (下期预告) |
|---|---|---|---|
| 输入数组 | 无重复 | 有重复 | 1-9 (无重复) |
| 元素复用 | 无限复用 | 不可复用 | 不可复用 |
| 目标 | 和为 target | 和为 target | 和为 n,且个数为 k |
| 排序 | 需要 (为了剪枝) | 必须 (为了去重+剪枝) | 不需要 (天然有序) |
| 递归参数 | i |
i + 1 |
i + 1 |
| 去重逻辑 | 无 | i > start && nums[i]==nums[i-1] |
无 |
LC 40 (本题) 是最能体现回溯算法"控制力"的一题。它要求我们像外科医生一样,精准地剔除重复的枝叶,同时保留合法的解。
下一篇,我们将快速解决 LC 216. 组合总和 III 。这道题增加了一个新的约束------"个数限制" (k)。这对我们的递归深度也是一种剪枝条件!
下期见!