【LeetCode 39】组合总和(Combination Sum)回溯算法从入门到彻底掌握
一、题目要求
给定一个 无重复元素 的整数数组 candidates 和一个目标值 target。
请找出所有可以使数字和为 target 的组合。
要求:
- 数组中的数字可以 无限次重复使用
- 返回所有不同组合
- 组合中元素顺序不重要(如
[2,3]和[3,2]算同一个)
示例
输入:
candidates = [2,3,6,7]
target = 7
输出:
[
[2,2,3],
[7]
]
解释:
2 + 2 + 3 = 7
7 = 7
二、核心思路
这道题本质是:
从数组中选数(可重复),让和等于 target
并且要找:
所有可能组合
这类问题的典型解法是:
回溯 + 深度优先搜索(DFS)
为什么用回溯?
因为这是一个:
枚举所有可能路径
的过程。
每一步都在做选择:
选哪个数?
选几次?
还能不能继续选?
如果某条路径走不通,就必须:
撤销选择,换一条路
这正是回溯的本质。
三、搜索树理解(最重要)
假设:
candidates = [2,3,6,7]
target = 7
搜索过程像一棵树:
[]
/ | | \
2 3 6 7
/ | \
2 3 6 ...
/ |
2 3
例如路径:
[] → 2 → 2 → 3 = 7 ✔
如果某条路径:
和 > target
直接剪枝(停止)。
四、关键设计思想
1. 用 sum 记录当前路径和
- 等于 target → 收集答案
- 大于 target → 停止
2. 用 start 控制选择范围(防止重复组合)
例如:
[2,3] 和 [3,2] 只能出现一个
解决办法:
只允许:
向后选,不回头选
3. 允许重复使用元素
注意这里:
dfs(i, ...)
不是:
dfs(i + 1, ...)
说明当前元素还能继续用。
五、完整代码(JavaScript)
var combinationSum = function(candidates, target) {
const res = [];
function dfs(start, path, sum) {
if (sum === target) {
res.push([...path]);
return;
}
if (sum > target) return;
for (let i = start; i < candidates.length; i++) {
path.push(candidates[i]);
dfs(i, path, sum + candidates[i]); // 可以重复使用
path.pop(); // 回溯
}
}
dfs(0, [], 0);
return res;
};
六、逐行代码详解
定义结果集
const res = [];
存放所有合法组合。
定义 DFS
function dfs(start, path, sum)
参数含义:
| 参数 | 含义 |
|---|---|
| start | 从哪个下标开始选 |
| path | 当前组合 |
| sum | 当前和 |
找到目标
if (sum === target) {
res.push([...path]);
return;
}
必须复制数组:
[...path]
否则回溯会修改结果。
剪枝
if (sum > target) return;
超过目标,停止搜索。
枚举选择
for (let i = start; i < candidates.length; i++)
只从当前位置往后选,避免重复组合。
做选择
path.push(candidates[i]);
加入当前数字。
继续搜索
dfs(i, path, sum + candidates[i]);
关键点:
i 不变
说明当前数字可以继续使用。
撤销选择(回溯核心)
path.pop();
恢复现场,尝试下一个数。
启动搜索
dfs(0, [], 0);
从空路径开始。
七、运行过程示例
输入:
[2,3,6,7], target = 7
路径变化:
[]
[2]
[2,2]
[2,2,2] → 6
[2,2,2,2] → 8 ✘
回溯
[2,2,3] → 7 ✔
...
[7] ✔
最终:
[[2,2,3], [7]]
八、时间复杂度
这是一个组合枚举问题。
时间复杂度:
指数级
因为需要遍历所有可能组合。
剪枝能减少大量无效搜索。
九、为什么不会出现重复组合?
关键在:
for (let i = start; ...)
保证:
路径单调递增
不会出现:
[3,2]
因为选 3 之后不会回去选 2。
十、回溯模板总结
组合问题通用模板:
做选择
递归
撤销选择
也就是:
path.push()
dfs()
path.pop()