【Hot 100 刷题计划】 LeetCode 39. 组合总和 | C++ 回溯算法与 startIndex 剪枝

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] 是同一种组合,如果不加控制,极容易生成重复的答案。

本解法的巧妙亮点(强制降序/平序法):

  1. 首先在主函数中对整个数组进行升序排序。
  2. 在递归的 for 循环中,每次都从索引 0 开始从头遍历。
  3. 核心去重逻辑 :if (tmp.size() && can[i] > tmp.back()) return ;。我们在放入新元素时,强制要求新放入的元素不能大于当前路径的最后一个元素。
  4. 由于我们是从小到大遍历的,遇到比队尾大的元素直接 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;
    }
};
相关推荐
海绵宝宝转agent14 分钟前
leetCode100算法开源笔记分享
笔记·算法
jimy117 分钟前
基类没有虚析构函数,基类指针删除派生类对象,行为是未定义的
开发语言·c++
写后端的胖头鱼37 分钟前
时间复杂度 & 空间复杂度
java·数据结构·算法·时间复杂度·空间复杂度
hahaha601639 分钟前
黑体轨迹和色温--光源白点
人工智能·嵌入式硬件·算法·计算机视觉
海绵宝宝转agent41 分钟前
基于Redis ZSet+AOP+注解实现限流注解算法
数据库·redis·算法
xlq223221 小时前
全面复习2
c++
无名猿1 小时前
移动构造与移动赋值:把资源偷过来
c++·性能优化·内存管理·现代c++·语法基础
All for pursuit.2 小时前
【回溯-7】494.目标和
数据结构·c++·算法·leetcode
白杨尚青2 小时前
C++入门篇(十三):vector(上)——动态数组:构造、空间增长与迭代器失效
开发语言·c++·笔记·算法·stl
Jasmine_llq2 小时前
《P17461 [GESP202609 八级] 生成树计数》
算法·栈·tarjan 算法·边双连通分量·深度优先搜索 dfs·tarjan 保存当前路径节点·模乘法