216. 组合总和 III
问题描述
找出所有相加之和为 n
的 k
个数的组合,且满足:
- 只使用数字 1 到 9
- 每个数字最多使用一次
解集不能包含重复的组合。
示例:
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。
示例 3:
输入: k = 4, n = 1
输出: []
解释: 不存在有效的组合。
在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。
算法思路
回溯法(DFS):
- 从数字 1 开始遍历到 9
- 将当前数字加入组合
- 递归处理后续数字(从当前数字+1开始)
- 回溯时移除最后添加的数字
- 当组合大小等于
k
且和为n
时保存结果
剪枝优化
:
- 和剪枝:当当前和超过
n
时终止当前分支 - 数量剪枝:当剩余数字数量不足以填满组合时提前终止
剩余数字数量
=9 - i + 1
还需数字数量
=k - path.size()
- 剪枝条件:
9 - i + 1 < k - path.size()
代码实现
class Solution {
public List<List<Integer>> combinationSum3(int k, int n) {
List<List<Integer>> result = new ArrayList<>();
backtrack(k, n, 1, 0, new ArrayList<>(), result);
return result;
}
/**
* 回溯核心方法
*
* @param k 组合大小
* @param n 目标和
* @param start 当前起始数字
* @param sum 当前组合和
* @param path 当前组合
* @param result 结果集
*/
private void backtrack(int k, int n, int start, int sum,
List<Integer> path, List<List<Integer>> result) {
// 终止条件1:组合大小等于k
if (path.size() == k) {
// 检查组合和是否等于n
if (sum == n) {
result.add(new ArrayList<>(path));
}
return;
}
// 遍历可选择的数字(从start到9)
for (int i = start; i <= 9; i++) {
// 剪枝1:当前和超过目标值
if (sum + i > n) {
break;
}
// 剪枝2:剩余数字不足
if (9 - i + 1 < k - path.size()) {
break;
}
// 选择当前数字
path.add(i);
// 递归处理后续数字
backtrack(k, n, i + 1, sum + i, path, result);
// 回溯:移除最后添加的数字
path.remove(path.size() - 1);
}
}
}
算法分析
- 时间复杂度:O(C(9,k) × k)
- 组合数 C(9,k) = 9!/(k!(9-k)!)
- 每个组合需要 O(k) 时间复制到结果
- 空间复杂度:O(k)
- 递归栈深度 O(k)
- 路径列表 O(k)
算法过程
k=3, n=7
开始:start=1, sum=0, path=[]
├─ i=1: path=[1], sum=1
│ ├─ i=2: path=[1,2], sum=3
│ │ ├─ i=3: 1+2+3=6 <7 → 组合大小等于k→ 回溯到第2层
│ │ ├─ i=4: 1+2+4=7 → 保存[1,2,4]
│ │ ├─ i=5: 1+2+5=8 >7 → 剪枝
│ └─ i=3: path=[1,3], sum=4
│ ├─ i=4: 1+3+4=8 >7 → 剪枝
└─ i=2: path=[2], sum=2
└─ i=3: path=[2,3], sum=5
└─ i=4: 2+3+4=9 >7 → 剪枝
最终结果:[[1,2,4]]
关键点
- 起始位置:每次递归从
i+1
开始,避免重复
使用数字 - 双重剪枝:
和剪枝
:sum + i > n
数量剪枝
:9 - i + 1 < k - path.size()
- 结果保存:当
path.size()==k
且sum==n
时保存 - 回溯操作:递归后移除最后添加的数字
剪枝原理
数量剪枝(k=3, path.size()=1):
- 还需数字数量 = 3 - 1 = 2
- 剩余数字数量 = 9 - 2 + 1 = 8(从2开始:2,3,…,9)
- 实际遍历范围:i 从 2 到 8
- i=8:剩余数字[9](1个 < 2)→ 剪枝
- i=9:剩余数字[](0个 < 2)→ 剪枝
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
System.out.println("Test 1 (k=3,n=7): " + solution.combinationSum3(3, 7));
// 预期:[[1,2,4]]
// 测试用例2:多解情况
System.out.println("Test 2 (k=3,n=9): " + solution.combinationSum3(3, 9));
// 预期:[[1,2,6],[1,3,5],[2,3,4]]
// 测试用例3:无解情况
System.out.println("Test 3 (k=4,n=1): " + solution.combinationSum3(4, 1)); // []
// 测试用例4:边界值
System.out.println("Test 4 (k=1,n=5): " + solution.combinationSum3(1, 5)); // [[5]]
// 测试用例5:最大组合
System.out.println("Test 5 (k=9,n=45): " + solution.combinationSum3(9, 45));
// 预期:[[1,2,3,4,5,6,7,8,9]]
// 测试用例6:超出范围
System.out.println("Test 6 (k=2,n=18): " + solution.combinationSum3(2, 18)); // []
}
常见问题
-
为什么需要
start
参数?
避免组合重复(如 [1,2] 和 [2,1]),保证组合内数字升序排列。 -
剪枝条件如何推导?
- 和剪枝:
sum + i > n
- 数量剪枝:剩余数字数量
9-i+1
必须 ≥ 还需数字数量k-path.size()
- 和剪枝:
-
为什么数量剪枝放在循环内?
因为path.size()
在循环中动态变化
,需在每次迭代时重新计算。 -
能否使用动态规划?
可以,但回溯法更直观且空间效率更高,因为需要输出所有组合而非计数
。 -
大数情况下是否会栈溢出?
k
最大为 9,递归深度最多 9 层,不会栈溢出。