LeetCode 39. 组合总和:DFS回溯解法详解

拆解 LeetCode 经典回溯题------39. 组合总和,这道题是回溯算法的入门必练题目,核心考察「无重复组合」与「元素可重复选取」的处理逻辑,学会这道题,能轻松应对一类回溯组合问题。

话不多说,先看题目本身,帮大家理清需求、避开陷阱。

一、题目解读:明确需求与边界

题目给出两个核心输入:无重复元素的整数数组 candidates,和目标整数 target。要求找出 candidates 中,所有能使数字和为 target 的不同组合,返回格式为列表,且组合顺序无要求。

这里有两个关键细节,也是解题的核心:

  • 「同一个数字可无限制重复选取」:比如 candidates 有 2,target 为 4,那么 [2,2] 是合法组合。

  • 「不同组合的判定」:至少一个数字的选取数量不同,即为不同组合。比如 [2,3] 和 [3,2] 视为同一组合(题目允许任意顺序返回),而 [2,2,3] 和 [2,3,3] 是不同组合。

另外题目给出约束:组合数少于 150 个,无需考虑极端情况下的性能优化,专注于回溯逻辑即可。

二、解题思路:为什么用回溯法?

这道题的本质是「从数组中挑选元素,允许重复选,凑出目标和」,属于「组合搜索」问题------我们需要遍历所有可能的选取方式,找到符合条件的组合,而回溯法正是处理这类「多路径搜索、需回退」问题的最优思路。

回溯法的核心逻辑的是「试探-回退-再试探」,可以类比为「走迷宫」:

  1. 试探:挑选一个元素,加入临时组合,计算当前和;

  2. 判断:如果当前和超过 target,说明此路径无效,直接回退;如果等于 target,将临时组合加入结果集,再回退;

  3. 回退:移除最后一个加入的元素,尝试下一个元素,继续试探。

这里有个关键优化点:如何避免出现重复组合(比如 [2,3] 和 [3,2])?

答案是「固定选取顺序」------让组合中的元素「非递减」排列(或非递增),具体做法是:在递归时,从当前元素的索引开始遍历,不再回头遍历前面的元素。这样就能保证,每个组合的元素顺序一致,不会出现重复。

三、完整代码与逐行解析

先给出完整可运行的 TypeScript 代码(与题目给出的代码一致,重点解析核心逻辑):

typescript 复制代码
function combinationSum(candidates: number[], target: number): number[][] {
    const res: number[][] = []; // 存储最终结果集

    // 回溯函数:start=当前开始遍历的索引,temp=临时组合,sum=当前组合的和
    const dfs = (start: number, temp: number[], sum: number) => {
        // 终止条件1:当前和超过target,无效路径,直接返回
        if (sum > target) {
            return;
        }
        // 终止条件2:当前和等于target,将临时组合存入结果集(浅拷贝,避免引用污染)
        else if (sum === target) {
            res.push([...temp]);
            return;
        }

        // 遍历:从start开始,避免重复组合
        for (let i = start; i < candidates.length; i++) {
            temp.push(candidates[i]); // 试探:加入当前元素
            dfs(i, temp, sum + candidates[i]); // 递归:继续从i开始(允许重复选当前元素)
            temp.pop(); // 回退:移除最后一个元素,尝试下一个选项
        }
    }

    dfs(0, [], 0); // 初始调用:从索引0开始,临时组合为空,当前和为0
    return res;
};

逐行解析核心细节

1. 结果集与回溯函数定义

定义res 数组存储最终的组合列表,回溯函数 dfs 接收三个参数:

  • start:当前开始遍历的索引,核心作用是「避免重复组合」,确保每次递归只从当前元素及之后的元素选取;

  • temp:临时组合,用于存储当前正在试探的组合;

  • sum:当前临时组合的数字和,用于快速判断是否达到目标。

2. 终止条件(回溯的"出口")

回溯函数必须有明确的终止条件,否则会陷入无限递归:

  • sum > target:当前组合的和已经超过目标,再继续添加元素只会更大,直接返回(剪枝,减少无效遍历);

  • sum === target:当前组合符合要求,将其存入结果集。注意这里用 [...temp] 浅拷贝,因为 temp 是引用类型,后续会被修改,不拷贝会导致结果集中的组合被覆盖。

3. 遍历与回溯核心逻辑

for 循环从 start 开始遍历 candidates 数组,这是避免重复组合的关键:

  1. temp.push(candidates[i]):将当前元素加入临时组合,进行「试探」;

  2. dfs(i, temp, sum + candidates[i]):递归调用,注意这里的 start 参数是 i 而非 i+1------因为允许重复选取当前元素,所以下一次递归仍可以从当前元素开始;

  3. temp.pop():「回退」操作,移除最后一个加入的元素,让循环继续尝试下一个元素,实现"回溯"。

4. 初始调用

dfs(0, [], 0):初始状态下,从数组索引 0 开始遍历,临时组合为空,当前和为 0,正式启动回溯过程。

四、关键易错点提醒

这道题看似简单,但新手很容易踩坑,重点注意以下3点:

  • 避免重复组合:必须从start 开始遍历,不能从 0 开始,否则会出现 [2,3] 和 [3,2] 这样的重复组合;

  • 临时组合浅拷贝:存入结果集时,一定要用 [...temp] 拷贝,否则后续 temp.pop() 会修改结果集中的组合;

  • 剪枝优化:当 sum > target 时直接返回,避免无效递归,提升效率(虽然题目约束组合数少于150,但剪枝是回溯题的必备思维)。

五、示例验证与拓展思考

示例验证

假设输入:candidates = [2,3,6,7], target = 7

按照代码逻辑,最终返回结果为 [[2,2,3], [7]],完全符合题目要求:

  • 2,2,3\]:2+2+3=7,元素可重复选取;

拓展思考

这道题的变种很多,比如:

  • 如果 candidates 有重复元素,如何避免重复组合?(LeetCode 40. 组合总和 II)

  • 如果限制每个元素只能选取一次,如何修改代码?

核心思路不变,只需调整遍历逻辑(比如去重、start 改为 i+1)。

六、总结

LeetCode 39. 组合总和的核心是「回溯法 + 剪枝 + 避免重复组合」,解题关键在于:

  1. 用回溯法遍历所有可能的组合,实现「试探-回退」;

  2. 通过start 参数固定选取顺序,避免重复组合;

  3. 及时剪枝(sum > target 时返回),提升效率。

这道题是回溯算法的入门经典,建议大家亲手敲一遍代码,修改参数(比如改变 candidates 和 target),观察回溯过程中的 temp 和 sum 变化,就能彻底理解回溯的逻辑。

相关推荐
Wect2 小时前
LeetCode 46. 全排列:深度解析+代码拆解
前端·算法·typescript
IT_陈寒2 小时前
Vite 凭什么比 Webpack 快50%?揭秘闪电构建背后的黑科技
前端·人工智能·后端
颜酱2 小时前
Dijkstra 算法:从 BFS 到带权最短路径
javascript·后端·算法
hi大雄2 小时前
我的 2025 —— 名为《开始的勇气》🌱
前端·年终总结
从文处安3 小时前
「前端何去何从」一直写 Vue ,为何要在 AI 时代去学 React?
前端·react.js
aircrushin3 小时前
OpenClaw“养龙虾”现象的社会技术学分析
前端·后端
明君879973 小时前
#Flutter 的官方Skills技能库
前端·flutter
yuki_uix3 小时前
重新认识 React Hooks:从会用到理解设计
前端·react.js
林太白3 小时前
ref和reactive对比终于学会了
前端