【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;
    }
};
相关推荐
黎阳之光2 小时前
视频孪生领航者,以中国技术定义全球数智化新高度
大数据·人工智能·算法·安全·数字孪生
患得患失9492 小时前
【前端WebSocket】心跳功能,心跳重置策略、双向确认(Ping-Pong) 以及 指数退避算法(Exponential Backoff)
前端·websocket·算法
海砥装备HardAus2 小时前
飞控算法中双环串级PID深度解析:角度环与角速度环的协同机制
stm32·算法·无人机·飞控·串级pid
宵时待雨2 小时前
优选算法专题1:双指针
数据结构·c++·笔记·算法·leetcode
zsc_1182 小时前
pvz3解码小游戏求解算法
算法
程序员学习随笔2 小时前
深入剖析 std::optional:实现原理、性能优化与安全编程实践
c++·安全·空值
汀、人工智能2 小时前
[特殊字符] 第107课:LRU缓存(最后一课[特殊字符])
数据结构·算法·链表·数据库架构·哈希表·lru缓存
数据知道2 小时前
claw-code 源码分析:结构化输出与重试——`structured_output` 一类开关如何改变「可解析性」与失败语义?
算法·ai·claude code·claw code
tankeven2 小时前
HJ172 小红的矩阵染色
c++·算法