LeetCode 39. 组合总和
📌 题目描述
题目级别:中等
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
- 示例 1:
输入:candidates = [2,3,6,7],target = 7
输出:[[2,2,3],[7]]
💡 解法一:DFS 结合平降序去重
这道组合题最麻烦的地方在于"去重"。因为 [2, 2, 3] 和 [3, 2, 2] 是同一种组合,如果不加控制,极容易生成重复的答案。
本解法的巧妙亮点(强制降序/平序法):
- 首先在主函数中对整个数组进行升序排序。
- 在递归的
for循环中,每次都从索引0开始从头遍历。 - 核心去重逻辑 :
if (tmp.size() && can[i] > tmp.back()) return ;。我们在放入新元素时,强制要求新放入的元素不能大于当前路径的最后一个元素。 - 由于我们是从小到大遍历的,遇到比队尾大的元素直接
return结束循环。这就保证了我们生成的路径永远是降序或者平序的(比如先选3,再选2,再选2,绝不会生成2,3,2),从而完美巧妙地避开了重复组合!
💻 C++ 代码实现 (巧妙降序法)
cpp
class Solution {
public:
vector<vector<int>> res;
vector<int> tmp;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
// 先排序,这是后续使用大小比较来进行去重和剪枝的基础
sort(candidates.begin(), candidates.end());
dfs(candidates, target, 0);
return res;
}
// 提示:这里加上了引用 &,防止在极深递归时发生大量的 vector 拷贝导致的 TLE
void dfs(const vector<int>& can, int target, int sum)
{
if (sum > target) return ;
if (sum == target)
{
res.push_back(tmp);
return ;
}
// 每次都从头开始选
for (int i = 0; i < can.size(); i ++ )
{
// 绝妙去重:强制生成的序列是降序/平序的,避免组合重复
if (tmp.size() && can[i] > tmp.back()) return ;
tmp.push_back(can[i]);
dfs(can, target, sum + can[i]);
tmp.pop_back(); // 回溯
}
}
};
💡 解法二:回溯算法与 startIndex 极致剪枝
在大厂面试中,对于"组合问题",面试官最期望看到的标准解法是引入 startIndex。
1. 使用 startIndex 去重
组合是不讲究顺序的。为了不搜出重复的组合,我们规定:在遍历的过程中,下一层递归只能从当前元素或当前元素之后的元素开始选,绝不回头看!
比如我们当前选了 3,那接下来的递归中,我们就只能选 3 以及 3 后面的数,彻底杜绝了 [3, 2, 2] 这种倒退选的重复路径。
2. 极致性能剪枝 (Pruning)
如果在没到达终点前,当前的 sum 已经大于 target 了,那就没必要往下搜了。
更极致的做法是:先对数组排序。在 for 循环里,如果发现 sum + candidates[i] > target,那不仅当前元素不用看了,因为后面的元素比当前元素更大,所以后面的所有循环分支都可以直接截断跳出!
💻 C++ 代码实现 (标准剪枝模板)
cpp
class Solution {
private:
vector<vector<int>> res; // 存放最终的所有组合
vector<int> path; // 存放当前正在探索的路径
void dfs(const vector<int>& candidates, int target, int sum, int startIndex) {
// 找到了和为 target 的组合
if (sum == target) {
res.push_back(path);
return;
}
// 横向遍历:从 startIndex 开始,保证绝不回头,天然去重
// 极致剪枝:如果 sum 加上当前数字已经超过了 target,由于数组有序,后面的数字更不用看了,直接结束循环
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
path.push_back(candidates[i]); // 处理节点
// 纵向递归:因为可以重复选当前数字,所以下一层搜索的起点仍然是 i,而不是 i + 1
dfs(candidates, target, sum + candidates[i], i);
path.pop_back(); // 回溯:撤销处理,尝试同层级的下一个数字
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
res.clear();
path.clear();
// 剪枝的大前提:必须先对数组进行升序排序!
sort(candidates.begin(), candidates.end());
// 初始状态:sum 为 0,startIndex 从 0 开始搜索
dfs(candidates, target, 0, 0);
return res;
}
};