【LeetCode 39】组合总和(Combination Sum)回溯算法从入门到彻底掌握

5 阅读3分钟

【LeetCode 39】组合总和(Combination Sum)回溯算法从入门到彻底掌握


一、题目要求

给定一个 无重复元素 的整数数组 candidates 和一个目标值 target

请找出所有可以使数字和为 target 的组合。

要求:

  1. 数组中的数字可以 无限次重复使用
  2. 返回所有不同组合
  3. 组合中元素顺序不重要(如 [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()