题目描述
给定一个无重复元素 的整数数组 candidates 和一个目标整数 target,找出 candidates 中可以使数字和为目标数 target 的所有不同组合,并以列表形式返回。
关键规则:
candidates中的同一个数字可以无限制重复被选取。- 如果至少一个数字的被选数量不同,则两种组合是不同的。
- 保证和为
target的不同组合数少于 150 个。
示例 1:
- 输入:
candidates = [2,3,6,7], target = 7 - 输出:
[[2,2,3],[7]] - 解释:2+2+3=7,7=7,仅这两种组合。
示例 2:
- 输入:
candidates = [2,3,5], target = 8 - 输出:
[[2,2,2,2],[2,3,3],[3,5]]
解法:回溯法(深度优先搜索)
核心思路
这道题本质上是带条件的组合问题,可以用回溯法解决:
- 路径记录 :用一个列表
path保存当前正在构建的组合。 - 和的跟踪 :用一个变量
sum记录当前路径的和。 - 终止条件 :
- 当
sum == target:将当前path加入结果集。 - 当
sum > target:直接返回(剪枝)。
- 当
- 可重复选取:为了避免重复组合,搜索时只能从当前元素开始向后遍历(不能回头),这样既保证了元素可重复使用,又不会出现排列重复的情况。
代码实现(C++)
class Solution {
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> res;
vector<int> path;
backtrack(candidates, target, 0, 0, path, res);
return res;
}
private:
/**
* @param candidates 候选数组
* @param target 目标和
* @param start 本次搜索的起始位置(避免重复组合)
* @param sum 当前路径的和
* @param path 当前路径
* @param res 结果集
*/
void backtrack(const vector<int>& candidates, int target, int start, int sum,
vector<int>& path, vector<vector<int>>& res) {
// 终止条件1:和等于目标,记录结果
if (sum == target) {
res.push_back(path);
return;
}
// 终止条件2:和超过目标,剪枝
if (sum > target) {
return;
}
// 从start开始遍历,避免重复组合
for (int i = start; i < candidates.size(); ++i) {
// 选择当前元素
path.push_back(candidates[i]);
sum += candidates[i];
// 递归搜索:下一层仍从i开始(可重复选取)
backtrack(candidates, target, i, sum, path, res);
// 回溯:撤销选择
sum -= candidates[i];
path.pop_back();
}
}
};
优化版(排序 + 剪枝)
先对数组排序,在循环中提前终止不可能的分支,提高效率:
class Solution {
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> res;
vector<int> path;
sort(candidates.begin(), candidates.end()); // 排序
backtrack(candidates, target, 0, 0, path, res);
return res;
}
private:
void backtrack(const vector<int>& candidates, int target, int start, int sum,
vector<int>& path, vector<vector<int>>& res) {
if (sum == target) {
res.push_back(path);
return;
}
for (int i = start; i < candidates.size(); ++i) {
if (sum + candidates[i] > target) break; // 剪枝:后面的元素更大,直接终止
path.push_back(candidates[i]);
backtrack(candidates, target, i, sum + candidates[i], path, res);
path.pop_back();
}
}
};
详细步骤解析(以 candidates = [2,3,6,7], target = 7 为例)
1. 初始状态
path = [],sum = 0,start = 0
2. 第一次递归(i=0,元素 2)
- 选择
2:path = [2],sum = 2 - 递归调用
backtrack,start = 0(可继续选 2)
第二次递归(i=0,元素 2)
- 选择
2:path = [2,2],sum = 4 - 递归调用
backtrack,start = 0
第三次递归(i=0,元素 2)
- 选择
2:path = [2,2,2],sum = 6 - 递归调用
backtrack,start = 0- 第四次递归:选
2→sum=8>7,剪枝返回 - 回溯,弹出
2,sum=6 i=1,选3→sum=6+3=9>7,剪枝返回- ... 后续元素均大于剩余和,返回
- 第四次递归:选
- 回溯,弹出
2,sum=4 i=1,选3→sum=4+3=7,path=[2,2,3],加入结果集
回溯过程
- 弹出
3,sum=4→ 继续遍历,后续元素过大,返回 - 弹出
2,sum=2→i=1,选3→sum=5,递归后无法凑够 7,返回 i=2,选6→sum=8>7,剪枝返回i=3,选7→sum=9>7,剪枝返回
3. 第一次递归(i=1,元素 3)
- 选
3:sum=3,后续无法凑够 7,返回 i=2,选6:sum=6,后续无法凑够 7,返回i=3,选7:sum=7,path=[7],加入结果集
最终结果
[[2,2,3],[7]]
关键知识点解析
1. 为什么 start 参数能避免重复组合?
- 如果每次都从
0开始遍历,会出现[2,3]和[3,2]这样的重复组合。 - 从
start开始遍历,保证了组合的元素顺序与原数组一致,不会出现排列重复。 - 同时,递归时仍从
i开始,允许元素重复选取。
2. 回溯的本质:选择 - 递归 - 撤销选择
- 选择 :将当前元素加入
path,更新sum。 - 递归:进入下一层搜索。
- 撤销选择 :弹出当前元素,恢复
sum,尝试下一个元素。
3. 剪枝优化的原理
- 对数组排序后,如果当前元素
candidates[i]已经大于target - sum,后面的元素会更大,无需继续遍历,直接break。 - 大幅减少无效递归调用,提升效率。
复杂度分析
- 时间复杂度:\(O(n \times 2^n)\),其中 n 是数组长度。每个元素都有选或不选两种可能,最坏情况下需要遍历所有组合。
- 空间复杂度:\(O(target / min(candidates))\),递归栈深度最多为目标和除以最小元素,即最长组合的长度。
常见误区与注意事项
- 忘记设置
start参数 :会导致重复组合,如[2,3]和[3,2]。 - 递归时
start设为i+1:会变成 "元素不可重复选取" 的组合问题,不符合题意。 - 未进行剪枝优化 :对于较大的
target和数组,会超时。
总结
这道题是回溯算法的经典入门题,核心是理解:
- 如何用
start参数控制组合顺序,避免重复。 - 如何利用排序 + 剪枝优化搜索效率。
- 回溯的 "选择 - 递归 - 撤销选择" 模板。
