【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;
    }
};
相关推荐
Merlos_wind3 小时前
HashMap详解
算法·哈希算法·散列表
汉克老师4 小时前
GESP2025年3月认证C++五级( 第三部分编程题(1、平均分配))
c++·算法·贪心算法·排序·gesp5级·gesp五级
Yzzz-F6 小时前
Problem - 2205D - Codeforces
算法
智者知已应修善业7 小时前
【51单片机2个按键控制流水灯运行与暂停】2023-9-6
c++·经验分享·笔记·算法·51单片机
Halo_tjn7 小时前
Java Set集合相关知识点
java·开发语言·算法
生成论实验室8 小时前
《事件关系阴阳博弈动力学:识势应势之道》第四篇:降U动力学——认知确定度的自驱演化
人工智能·科技·神经网络·算法·架构
AI科技星8 小时前
全域数学·72分册:场计算机卷【乖乖数学】
算法·机器学习·数学建模·数据挖掘·量子计算
云泽8088 小时前
C++11 核心特性全解:列表初始化、右值引用与移动语义实战
开发语言·c++
科研前沿9 小时前
镜像孪生VS视频孪生核心技术产品核心优势
大数据·人工智能·算法·重构·空间计算
水蓝烟雨9 小时前
1931. 用三种不同颜色为网格涂色
算法·leetcode