给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 **不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例 1:
输入: candidates = [2,3,6,7], target = 7
输出: [[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2], target = 1
输出: []
提示:
1 <= candidates.length <= 302 <= candidates[i] <= 40candidates的所有元素 互不相同1 <= target <= 40
1. 生活案例:自助找零机
想象你正在一台自助找零机前,你需要凑齐 7块钱(target)。
-
你的钱包(candidates) :里面有无尽的
2块、3块、6块和7块面值的硬币。 -
规则:
- 你可以重复拿同一种硬币(比如拿三次 2 块)。
- 你需要列出所有能刚好凑成 7 块的组合。
-
过程:
- 你先拿一个
2块,还差 5 块;再拿一个2块,还差 3 块;再拿一个2块,还差 1 块。 - 剩下的硬币里最小也是 2 块,拿了就超标了(8块)。
- 于是你退回一步(回溯),不拿第三个 2 块了,试着换成一个
3块。 - 刚好凑成 !记录下来。
- 你继续退回,尝试其他的组合,直到所有可能性都试遍。
- 你先拿一个
2. 代码实现与详细注释
这是你图片中的代码,我为你加上了结合“找零”逻辑的详细中文注释:
JavaScript
/**
* @param {number[]} candidates - 可选的硬币面值
* @param {number} target - 目标金额
* @return {number[][]}
*/
var combinationSum = function(candidates, target) {
let res = [];
// 【优化点】:先从小到大排序,这样如果加了一个数已经超了,后面的数就不用看了
candidates.sort((a, b) => a - b);
/**
* @param {number} start - 从哪种面值的硬币开始选(防止重复组合,如 [2,3] 和 [3,2])
* @param {number[]} path - 已经放进兜里的硬币组合
* @param {number} sum - 兜里硬币的总面值
*/
let dfs = (start, path, sum) => {
// 【成功出口】:刚好凑齐
if (sum === target) {
res.push([...path]); // 记录方案
return;
}
// 尝试从当前的 start 位置开始选硬币
for (let i = start; i < candidates.length; i++) {
// 【剪枝优化】:如果当前的钱加上这枚硬币已经超了,后面的硬币更大,直接放弃
if (sum + candidates[i] > target) break;
// 1. 【做选择】:把这枚硬币放进兜里
path.push(candidates[i]);
// 2. 【递归】:继续选下一枚。
// 注意:这里传的是 i 而不是 i + 1,表示【可以重复使用】当前这枚硬币
dfs(i, path, sum + candidates[i]);
// 3. 【回溯】:把最后放进去的硬币拿出来,尝试换别的
path.pop();
}
}
dfs(0, [], 0); // 从第 0 种硬币、空兜、总和 0 开始
return res;
};
3. 核心原理解析
为什么递归传的是 i 而不是 i + 1?
这是本题的关键!
- 如果传
i + 1:代表每种硬币只能用一次(LeetCode 第 40 题)。 - 传
i:代表你在选完这枚硬币后,下一轮还可以选这枚硬币,从而实现了“无限重复选取”。
为什么要先 sort 排序?
如果不排序,你必须把整个树走完才能知道超没超标。
排序后,一旦发现 sum + candidates[i] > target,根据从小到大的特性,i 之后的所有硬币肯定也都会超标。这时候直接 break 循环(这就是所谓的剪枝),能极大地节省计算时间。
复杂度分析
- 时间复杂度:,其中 是所有可行解的长度之和。回溯算法的时间复杂度通常较高,但剪枝能显著提升表现。
- 空间复杂度:。主要是递归的栈深度。
总结
这道题是理解回溯中“去重”和“重复利用”逻辑的最好教材。只要记住:为了不选重复的组合,我们不走回头路(通过 start 控制);为了能重复拿,我们允许原地踏步(通过 dfs(i, ...) 控制)。