题目描述
给你一个 无重复元素 的整数数组 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 <= 30
- 2 <= candidates[i] <= 40
- candidates 的所有元素 互不相同
- 1 <= target <= 40
思考一:递归回溯(无限选→有限选转化)
核心思路是将“元素可无限选”转化为“按target限制的有限选”:
- 对每个元素,通过
Math.floor(target / candidates[i])计算其最大可选次数(选多了会超target,无需考虑),把无限选问题转为“选1~k次”的有限问题; - 按数组索引顺序递归(从start开始),避免重复组合(如[2,3]和[3,2]);
- 用sum实时记录当前组合和,超target则剪枝,等于target则记录结果,实现高效回溯。
算法过程
- 初始化:结果列表
result,当前组合t,当前和sum; - 递归入口:从索引0调用
dfs(0),开始处理第一个元素; - 递归逻辑(处理索引start及之后元素):
- 剪枝/终止:sum>target直接返回;sum===target,将
t副本加入result返回; - 遍历元素:从start遍历数组,计算当前元素最大可选次数k;
- 枚举选次:对每个元素,枚举选1~k次的情况:
- 累加sum、将元素重复j次加入
t,递归处理下一个元素(i+1); - 回溯:减去sum、从
t中删除j个该元素,恢复状态;
- 累加sum、将元素重复j次加入
- 剪枝/终止:sum>target直接返回;sum===target,将
- 返回结果:递归结束后,
result包含所有合法组合,返回即可。
时空复杂度
- 时间复杂度:O(target²·n)(n为候选数个数)
每个元素最大选次约为target/candidates[i](最坏O(target)),递归深度约为n,每个组合生成需O(target)时间(复制数组),总复杂度近似O(target²·n)。 - 空间复杂度:O(target)
递归栈深度最大为target(如全选1,需target个元素),t的最大长度也为target,额外空间为O(target)。
代码
/**
* @param {number[]} candidates
* @param {number} target
* @return {number[][]}
*/
var combinationSum = function(candidates, target) {
const result = [];
const n = candidates.length;
let t = [];
let sum = 0;
const dfs = function(start) {
if (sum > target) return;
if (sum === target) {
result.push([...t]);
return;
}
for (let i = start; i < n; i++) {
const k = Math.floor(target / candidates[i]);
for (let j = 1; j <= k; j++) {
sum += candidates[i] * j;
t.push(...Array(j).fill(candidates[i]));
dfs(i+1);
sum -= candidates[i] * j;
for (let l = 1; l <= j; l++) {
t.pop();
}
}
}
};
dfs(0);
return result;
};
思考二:递归回溯(可重复选+排序剪枝)
核心思路是通过“递归索引不递增”实现元素可重复选,结合排序剪枝减少无效分支:
- 可重复选的关键:递归调用时传入当前元素索引
i(而非i+1),允许下一轮仍选择当前元素,同时限制从start开始遍历,避免生成顺序不同的重复组合(如[2,3]和[3,2]); - 排序剪枝:对
candidates排序后,若当前元素与sum之和超过target,后续元素更大,可直接跳出循环,减少无效递归; - 实时维护
sum和t(当前组合),sum等于target时记录结果,回溯时恢复状态。
算法过程
- 初始化与排序:
- 初始化结果列表
result、当前组合t、当前和sum; - 对
candidates升序排序,为剪枝做准备。
- 初始化结果列表
- 递归入口:
调用dfs(0),从数组第0个元素开始处理(start=0确保组合无顺序重复)。 - DFS核心逻辑(处理索引
start及之后的元素):- 终止条件:若
sum === target,将t的副本加入result,返回; - 遍历元素:从
start遍历数组,对每个元素num:
① 剪枝:若sum + num > target,后续元素更大,直接跳出循环;
② 选择元素:sum累加num,num加入t;
③ 递归:调用dfs(i)(传当前索引i,允许重复选num);
④ 回溯:sum减去num,t弹出num,恢复状态。
- 终止条件:若
- 返回结果:递归结束后,
result包含所有合法组合,返回即可。
时空复杂度分析
-
时间复杂度:O(n×2(target/minval))O(n × 2^(target/min_val))O(n×2(target/minval))
n为candidates长度,min_val为candidates中的最小元素;- 每个元素的最大选择次数约为
target/min_val,总递归分支数近似 2(target/minval)2^(target/min_val)2(target/minval)(每个元素“选”或“不选”,但受target限制); - 每个组合生成时需复制
t(最长长度为target/min_val),实际操作次数可简化为 O(n × 2^(target/min_val)),排序的 O(n log n) 可忽略。
-
空间复杂度:O(target/minval)O(target/min_val)O(target/minval)
- 递归栈深度最大为
target/min_val(如全选最小元素,需target/min_val次递归); - 当前组合
t的最大长度也为target/min_val,额外空间仅为递归栈和t的开销(结果存储不计入额外空间)。
- 递归栈深度最大为
代码
/**
* @param {number[]} candidates
* @param {number} target
* @return {number[][]}
*/
var combinationSum = function(candidates, target) {
const result = [];
const n = candidates.length;
let t = [];
let sum = 0;
candidates.sort((a, b) => a - b);
const dfs = function(start) {
if (sum === target) {
result.push([...t]);
return;
}
for (let i = start; i < n; i++) {
const num = candidates[i];
if (sum + num > target) break;
sum += num;
t.push(num);
dfs(i);
sum -= num;
t.pop();
}
};
dfs(0);
return result;
};
1016

被折叠的 条评论
为什么被折叠?



