哈喽各位,我是前端小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。 -
约束:
-
每种组合中只包含
k个数字。 -
每个数字最多使用一次。
-
例子: 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,说明路走到了尽头但没找到宝藏,返回。
- 剪枝 (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。
- O(k)。递归深度为
总结:组合问题的"毕业典礼"
至此,我们已经攻克了所有经典的组合类回溯问题!
让我们来最后梳理一下这张**"组合问题决策表"**:
| 题目场景 | 关键策略 | 代码特征 |
|---|---|---|
| 找所有子集 (无重复) | 收集所有节点 | res.push(path) 放开头 |
| 找所有子集 (有重复) | 排序 + 树层去重 | if (i > start && nums[i] == nums[i-1]) |
| 找和为N的组合 (无限复用) | 原地递归 | backtrack(i) |
| 找和为N的组合 (不可复用) | 步步为营 | backtrack(i + 1) |
| 找个数为K的组合 | 深度限制 | if (path.size() == k) |
这张表,就是你应对面试中所有"组合/子集"问题的通关秘籍。
下一篇,我们将告别单纯的数字,进入更复杂的**"分割"**问题。如果给你一个字符串,让你把它切成若干个回文串,你该怎么切?这其实也是一个组合问题!
下期见!