LeetCode 216. 组合总和 III
题目描述


题意分析
从整数
1到9中选择恰好k个不同数字,使它们的和等于目标n,返回全部组合。每个数字最多使用一次,数字顺序不同但集合相同,仍算同一个组合。两个条件必须同时满足:数量正好是
k,总和正好是n。只达到目标和但没有选够,或选满数量后还有剩余目标,都不能收集。
解法:递增回溯剪枝
核心思路
[!blue]
将每条选择路径保持严格递增。选了当前数字后,下一层只尝试更大的数字,这既防止重复使用同一数字,也让每个无序组合只有一种递增表示,不需要在结果中再次去重。
递归状态
start表示接下来最小可选值,need表示还要选多少个数,remain表示这些数需要凑出的和。need == 0时结束,只在remain == 0时保存路径副本。剩余数量还能提供和的界限。最小可能选择是从
start开始的连续need个数,其和为(2 * start + need - 1) * need / 2;最大可能选择是最大的need个数,其和为(19 - need) * need / 2。剩余目标小于最小和或大于最大和,任何后续选择都不可能成功,可以整段剪去。枚举当前值时,最大只需到
10 - need:选中它之后,右侧至少还要留出need - 1个不同数字。若当前值已经大于剩余目标,后面的候选只会更大,且所有数为正,可以直接结束本层循环。做一次选择后减少数量和目标,递归回来再删除路径末尾,恢复兄弟分支的公共前缀。收集时复制路径,避免后续撤销改动已有答案。
解题步骤
- 从最小候选
1、剩余数量k、剩余目标n和空路径开始。- 数量已选满时检查剩余目标,满足则收集副本,无论是否满足都返回。
- 计算剩余数量能够形成的最小和、最大和,目标不在范围内就剪枝。
- 从
start枚举到10 - need,候选已经大于remain时停止循环。- 追加候选,递归到
num + 1、need - 1、remain - num;返回后撤销末尾元素。- 全部分支处理完,返回收集到的组合。
代码实现
class Solution {
// 状态只需要记录当前可选起点、还需要选择几个数、剩余目标和,以及当前路径。
public List<List<Integer>> combinationSum3(int k, int n) {
List<List<Integer>> res = new ArrayList<>();
backtrack(1, k, n, new ArrayList<>(), res);
return res;
}
private void backtrack(
int start, int need, int remain, List<Integer> path, List<List<Integer>> res) {
if (need == 0) {
if (remain == 0) {
// 复制当前方案,避免后续回溯修改已经收集的结果
res.add(new ArrayList<>(path));
}
return;
}
// 用剩余数量的最小与最大可能和剪掉不可能分支
int minSum = (start + start + need - 1) * need / 2;
int maxSum = (19 - need) * need / 2;
if (remain < minSum || remain > maxSum) {
return;
}
// 当前选择至多到十减剩余数量,为后续保留足够不同数字
for (int num = start; num <= 10 - need; num++) {
if (num > remain) {
break;
}
path.add(num);
backtrack(num + 1, need - 1, remain - num, path, res);
path.remove(path.size() - 1);
}
}
}
func combinationSum3(k int, n int) [][]int {
// 状态只需要记录当前可选起点、还需要选择几个数、剩余目标和,以及当前路径。
res := make([][]int, 0)
path := make([]int, 0, k)
var dfs func(start, need, remain int)
dfs = func(start, need, remain int) {
if need == 0 {
if remain == 0 {
pathCopy := make([]int, len(path))
// 复制当前方案,避免后续回溯修改已经收集的结果
copy(pathCopy, path)
res = append(res, pathCopy)
}
return
}
// 用剩余数量的最小与最大可能和剪掉不可能分支
minSum := (start + start + need - 1) * need / 2
maxSum := (19 - need) * need / 2
if remain < minSum || remain > maxSum {
return
}
// 当前选择至多到十减剩余数量,为后续保留足够不同数字
for num := start; num <= 10-need; num++ {
if num > remain {
break
}
path = append(path, num)
dfs(num+1, need-1, remain-num)
path = path[:len(path)-1]
}
}
dfs(1, k, n)
return res
}
复杂度分析
- 时间复杂度:上界为 $O(k\binom{9}{k})$。递增且预留足够数量的搜索只沿可扩展为
k元组合的前缀展开,每条完整组合对应至多k层;结果复制也需要k次操作。和的剪枝减少实际访问。- 空间复杂度:辅助空间为 $O(k)$,路径和递归深度至多为
k;返回结果另外需要与方案总长度对应的空间。
关键点总结
[!green]
- 用严格递增的路径表示组合,同时去掉元素重复和排列重复。
- 数量与和是独立的成功条件,递归状态分别保存。
- 用最小/最大可达和判断整段无解,用当前值上界预留后续名额。
- 追加与撤销保持对称,收集独立副本后才能安全继续搜索。
易错点总结
[!yellow]
- 下一层仍从当前数字开始,会再次选择同一个数字;必须从
num + 1开始。- 剩余和为零就立即收集,没有检查数量是否恰好用完。
- 选满数量后仍继续递归,会产生超出
k个数字的路径。- 最大当前值没有为剩余数量留位置,会探索大量不可能选满的分支。
- 最小和公式没有随
start更新,可能错误排除或放过当前区间的选择。- 直接保存可变路径对象或切片,后续回溯会改变先前结果,应复制路径内容。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 77. 组合 | 中等 | 同样固定选k个元素,本题还限制可选值为1到9并要求目标和。 |
| 39. 组合总和 | 中等 | 同样搜索目标和,原题允许候选重复使用,本题每个数最多一次且数量固定。 |
| 40. 组合总和 II | 中等 | 在选择和撤销之间枚举组合;本题固定元素数量且只取一到九,该题候选只能用一次且需去重。 |
| 78. 子集 | 中等 | 在选择和撤销之间枚举组合;本题固定元素数量且只取一到九,该题输出所有选择长度的子集。 |
| 90. 子集 II | 中等 | 在选择和撤销之间枚举组合;本题固定元素数量且只取一到九,该题排序后跳过同层重复值。 |
| 377. 组合总和 Ⅳ | 中等 | 组合总和系列。III 从 1 到 9 中选定数量且不重复;IV 从给定集合重复取数并统计有序方案。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!