回溯算法专题(六):双重剪枝的艺术——「组合总和 III」

哈喽各位,我是前端小L。

欢迎来到我们的回溯算法专题第六篇!我们已经打通了组合问题的各个关卡:

  • LC 77/78:基础组合。

  • LC 39:无限复用 -> 原地递归。

  • LC 40:含重复元素 -> 排序去重。

今天,我们要解决的是:在 1 到 9 中,找出所有和为 n 且个数恰好为 k 的组合。

这道题就像是在玩"数字凑凑看",但规则很死:只能用 k 张牌,牌面只能是 1-9。这为我们的回溯树带来了明确的深度限制。

力扣 216. 组合总和 III

https://leetcode.cn/problems/combination-sum-iii/

题目分析:

  • 候选池 :[1, 2, 3, 4, 5, 6, 7, 8, 9](天然有序,无重复)。

  • 目标 :和为 n。

  • 约束:

    1. 每种组合中只包含 k 个数字。

    2. 每个数字最多使用一次。

例子: k = 3, n = 7

  • [1, 2, 4] (1+2+4=7, 3个数) -> 符合

  • [1, 6] (和为7, 但只有2个数) -> 不符合

  • [2, 2, 3] (重复使用) -> 不符合

核心思维:双重 Base Case 与 双重剪枝

我们的递归函数 backtrack 需要关注两个状态:当前的和 currentSum 和 当前的元素个数 path.size()。

1. Base Case (递归终点)

  • 成功 :如果 path.size() == k 且 currentSum == n,说明我们找到了一个完美解!收集结果,返回。

  • 失败(深度截止) :如果 path.size() == k 但 currentSum != n,说明路走到了尽头但没找到宝藏,返回。

  1. 剪枝 (Pruning) ------ 高质量解法的关键

这道题有两个维度的剪枝机会:

  • 剪枝一:数值剪枝 (Sum Pruning)

    和之前一样,如果 currentSum > n,说明已经爆了,后面的数字更大,肯定也爆,直接返回。

  • 剪枝二:数量剪枝 (Count Pruning) (高阶技巧!)

    这是一个很容易被忽略的优化。

    • 假设 k = 3,我们已经选了 [1] (size=1)。我们需要再选 2 个数。

    • 如果 for 循环遍历到了 i = 9。

    • 从 9 开始,后面没有数字了。我们最多只能选到 [1, 9],个数是 2,永远凑不够 3 个数。

    • 所以,i 的遍历范围不需要到 9,只需要到"还能凑够 k 个数"的那个位置即可。

    • 公式 :i 最多遍历到 9 - (k - path.size()) + 1。

代码实现 (C++)

C++

复制代码
#include <vector>
#include <numeric>

using namespace std;

class Solution {
private:
    vector<vector<int>> res;
    vector<int> path;
    int currentSum = 0;

    void backtrack(int k, int targetSum, int startIndex) {
        // 1. 剪枝:数值已经超了
        if (currentSum > targetSum) {
            return;
        }

        // 2. Base Case:数量够了
        if (path.size() == k) {
            // 只有和也相等,才是正解
            if (currentSum == targetSum) {
                res.push_back(path);
            }
            return; // 无论和对不对,只要个数到了k,就不能再往下搜了
        }

        // 3. 遍历选择列表 (1 到 9)
        // 剪枝二:数量剪枝
        // i 最多能取到哪里?
        // 还需要选的个数 = k - path.size()
        // 剩余元素个数 (9 - i + 1) 必须 >= 还需要选的个数
        // 即:9 - i + 1 >= k - path.size()
        // 移项得:i <= 9 - (k - path.size()) + 1
        for (int i = startIndex; i <= 9 - (k - path.size()) + 1; ++i) {
            
            // 做选择
            path.push_back(i);
            currentSum += i;

            // 递归 (每个数字只能用一次,所以是 i + 1)
            backtrack(k, targetSum, i + 1);

            // 撤销选择
            currentSum -= i;
            path.pop_back();
        }
    }

public:
    vector<vector<int>> combinationSum3(int k, int n) {
        res.clear();
        path.clear();
        currentSum = 0;
        
        backtrack(k, n, 1); // 题目要求从 1 到 9
        
        return res;
    }
};

深度复杂度分析

  • 时间复杂度:

    • 这是一个有限集合 [1..9] 上的组合问题。

    • 最坏情况下是从 9 个数里选 k 个,即组合数 C(9, k)。

    • 由于 9 是一个非常小的常数,实际上这个算法的执行时间极短,可以视为 O(1) 或 O(k \\cdot C(9, k))。

    • (这也是为什么回溯算法在数据规模小的时候非常有效)。

  • 空间复杂度:

    • O(k)。递归深度为 k,path 数组大小为 k。

总结:组合问题的"毕业典礼"

至此,我们已经攻克了所有经典的组合类回溯问题!

让我们来最后梳理一下这张**"组合问题决策表"**:

题目场景 关键策略 代码特征
找所有子集 (无重复) 收集所有节点 res.push(path) 放开头
找所有子集 (有重复) 排序 + 树层去重 if (i > start && nums[i] == nums[i-1])
找和为N的组合 (无限复用) 原地递归 backtrack(i)
找和为N的组合 (不可复用) 步步为营 backtrack(i + 1)
找个数为K的组合 深度限制 if (path.size() == k)

这张表,就是你应对面试中所有"组合/子集"问题的通关秘籍。

下一篇,我们将告别单纯的数字,进入更复杂的**"分割"**问题。如果给你一个字符串,让你把它切成若干个回文串,你该怎么切?这其实也是一个组合问题!

下期见!

相关推荐
闻缺陷则喜何志丹4 小时前
详细设计说明书的内容要求
算法·测试·存储
和裕4 小时前
定制纸箱刀卡、平卡、隔档分别是什么?结构与适用场景区别全解析
大数据·运维·网络·人工智能·算法
Tisfy4 小时前
LeetCode 3723.数位平方和的最大值:构造
数学·算法·leetcode·字符串·题解
hetao17338374 小时前
联盟场-提高组-R4 & ZYZSC-S-Round 7
c++·算法
碧海银沙音频科技研究院4 小时前
基于炬芯ATS3089X音箱的音乐播放歌词显示功能实现
人工智能·驱动开发·嵌入式硬件·算法
知识分享小能手5 小时前
C学习教程,从入门到精通,C语言数组 —— 知识点详解(8)
c语言·学习·算法
hetao17338375 小时前
联盟场-提高组-R2 & ZYZSC-S-Round 5
c++·算法
其实防守也摸鱼5 小时前
Codex 下载与本地部署实战指南
linux·服务器·前端·数据库·算法·ai
南极企鹅5 小时前
生产环境gc文件分析
java·jvm·算法
希洛兔5 小时前
原码、反码、补码:一张图讲透计算机里的负数表示
算法·面试