LeetCode 40中等回溯 · 去重
组合总和 II 图解题解
这道题到底在问什么
候选数每个只能用一次,找和为 target 的不重复组合。
- candidates
- [10,1,2,7,6,1,5], target=8
- 输出
- [[1,1,6],[1,2,5],[1,7],[2,6]]
最优解:一步一步想明白
- 3关键不是背模板,而是看懂为什么这题适合回溯 · 去重。
- 4排序 → [1,1,2,5,6,7,10],相同数字相邻。target=8,每选一个数就从 remain 里减掉,remain 减到 0 即一个答案。每个数只用一次 → 递归传 i+1。
- 5选下标 0 的 1 → remain = 8 − 1 = 7。
- 6下一层(i+1)选下标 1 的 1 —— 它在更深层,不是同层重复,可以选 → remain 6。
- 7选下标 4 的 6 → remain 6 − 6 = 0 → 收集 [1,1,6]!
- 8撤销 6 回到 [1,1](remain 6),下一个候选是下标5的 7:7 > remain 6。排序后后面只会更大 → 直接 break 剪枝,不再往下试。
- 9下标0的 1 这条大分支全部走完,回到根。本想从下标1的 1 再开同样的分支,但它和同层下标0的 1 值相同 → 跳过(i > start 且 nums[i]==nums[i−1]),否则会重复生成一整批 [1,…]。
- 10换从下标2的 2 开分支:2 → 6 → remain 0 → 收集 [2,6]。(同样方式,途中还找到 [1,2,5]、[1,7])
- 11完整搜索得 4 个和为 8 的不重复组合:[[1,1,6],[1,2,5],[1,7],[2,6]]。靠「同层去重」防重复、「候选>remain 就 break」剪枝。
- 14把这句话记住,下次遇到同类题,就能更快选出方向。
⚠️ 容易写错的地方
✗ 错:去重时把所有相同数字都禁掉
✓ 对:只跳过同一层的重复选择
不同层可以选重复值的不同副本
✗ 错:用过的数下一层还从 i 开始选
✓ 对:传 i+1,每个数只用一次
本题每个数字限用一次;传 i 会把同一个数重复使用(那是组合总和 I)。
✗ 错:变量名和动画不一致
✓ 对:代码变量沿用动画里的核心名字
学习时最怕脑内维护两套概念
完整代码(Python / C++ / Java)
Python
class Solution:
def combinationSum2(self, candidates, target):
candidates.sort()
ans = []
def backtrack(start, remain, path):
if remain == 0:
ans.append(path[:]); return
for i in range(start, len(candidates)):
if i > start and candidates[i] == candidates[i - 1]:
continue # 同层去重
if candidates[i] > remain:
break # 排序后剪枝
path.append(candidates[i])
backtrack(i + 1, remain - candidates[i], path) # i+1: 只用一次
path.pop()
backtrack(0, target, [])
return ansC++
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void backtrack(vector<int>& c, int start, int remain) {
if (remain == 0) { ans.push_back(path); return; }
for (int i = start; i < c.size(); i++) {
if (i > start && c[i] == c[i-1]) continue; // 同层去重
if (c[i] > remain) break; // 排序后剪枝
path.push_back(c[i]);
backtrack(c, i + 1, remain - c[i]); // i+1: 只用一次
path.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
backtrack(candidates, 0, target);
return ans;
}
};Java
class Solution {
List<List<Integer>> ans = new ArrayList<>();
List<Integer> path = new ArrayList<>();
void backtrack(int[] c, int start, int remain) {
if (remain == 0) { ans.add(new ArrayList<>(path)); return; }
for (int i = start; i < c.length; i++) {
if (i > start && c[i] == c[i-1]) continue; // 同层去重
if (c[i] > remain) break; // 排序后剪枝
path.add(c[i]);
backtrack(c, i + 1, remain - c[i]); // i+1: 只用一次
path.remove(path.size() - 1);
}
}
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
backtrack(candidates, 0, target);
return ans;
}
}套路模板 · 回溯 · 去重
# 组合总和II骨架 · 同层去重 + 剪枝
candidates.sort()
def backtrack(start, remain, path):
if remain == 0: 收集 path; return
for i in range(start, n):
if i > start and c[i] == c[i-1]: continue # ① 同层去重
if c[i] > remain: break # ② 排序后剪枝
path.append(c[i]); backtrack(i+1, remain-c[i], path); path.pop() # i+1 只用一次复杂度
时间复杂度
O(2^n)
最坏每个数选/不选,O(2ⁿ),排序剪枝后实际远小于此
空间复杂度
O(n)
递归深度 + path,O(n)(不计输出)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 组合总和 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
和「组合总和 I」的两点区别?+
I 元素可重复用(传 i)、无重复元素;II 每个用一次(传 i+1)且有重复元素需同层去重。
为什么排序+同层跳过能去重?+
排序让相同数相邻;同层只用第一个相同数往下递归,后面会产生完全相同的组合、跳过。
剪枝优化?+
排序后若当前数已>剩余 target,后面更大、可直接 break。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。