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

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

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

  • LC 77/78:基础组合。

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

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

今天,我们要解决的是:19 中,找出所有和为 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)。递归深度为 kpath 数组大小为 k

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

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

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

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

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

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

下期见!

相关推荐
Rock_yzh26 分钟前
LeetCode算法刷题——53. 最大子数组和
java·数据结构·c++·算法·leetcode·职场和发展·动态规划
阿_旭27 分钟前
LAMP剪枝的基本原理与方法简介
算法·剪枝·lamp
leoufung32 分钟前
103. 二叉树的锯齿形层序遍历(LeetCode 103)
算法·leetcode·职场和发展
程序员东岸33 分钟前
《数据结构——排序(上)》从扑克牌到分治法:插入排序与希尔排序的深度剖析
数据结构·笔记·算法·排序算法
bxlj_jcj1 小时前
分布式ID方案、雪花算法与时钟回拨问题
分布式·算法
墨染点香1 小时前
LeetCode 刷题【179. 最大数】
算法·leetcode·职场和发展
失忆已成习惯.1 小时前
西农数据结构第四次实习题目参考
数据结构·算法·图论
kyle~1 小时前
排序---堆排序(Heap Sort)
数据结构·c++·算法
yesyesido1 小时前
3D在线魔方模拟器
科技·算法·3d·生活·业界资讯·交友·帅哥