LeetCode 77. 组合
题目描述
✅ 77. 组合

题意分析
输入两个整数
n和k,要输出从1到n这n个数中选出k个数的所有组合,答案可以按任意顺序返回。约束里最关键的信号是「组合」二字:
[1, 2]和[2, 1]被视为同一个答案,只能出现一次。这与排列题的根本区别,决定了枚举方式必须能天然规避重复,而不是先枚举全部排列再去重。另一个信号是1 <= k <= n <= 20,答案数量是 $C(n, k)$,最大出现在n = 20、k = 10时约 18 万条,规模上允许把每一条答案都真正构造出来,也就是说这是一道「输出全部方案」的枚举题,不存在比枚举更快的算法——下界就摆在答案个数上。边界上要考虑:
k等于n时答案只有一条,就是全选;k等于 1 时答案有n条;数字范围从1开始而不是0,起点别写错;答案里每条组合的内部顺序题目不作要求,但按递增输出最自然也最便于自查。
解法:回溯枚举组合
核心思路
组合只关心选了哪些数,不关心顺序。若每层都从 1 开始选择,
[1,2]和[2,1]会重复出现。解决方法是让路径始终严格递增:递归参数start表示本层可选的最小数字,选择num后,下一层只从num+1继续。循环不变量是:进入递归时,
path严格递增,且其中所有数都小于start。因此每个组合只对应唯一一条递归路径,既不重复也不遗漏。路径长度达到k时,必须复制当前路径加入答案,因为后续回溯还会修改原容器。还可以剪掉必然凑不满的分支。若当前还需要
\[n-num \ge need-1 \quad\Longrightarrow\quad num \le n-need+1\]need = k - path.size()个数,本层选择的num后面至少要剩下need-1个数:所以本层循环上界是
n-need+1,而不是n。
解题步骤
- 从
start=1、空路径开始递归。- 当路径长度等于
k时,复制路径加入答案并返回。- 计算还需选择的数量
need,只枚举start..n-need+1。- 对每个
num执行“加入路径 → 从num+1递归 → 删除路径末尾”。以
n=4, k=2为例,第一层还需 2 个数,上界为4-2+1=3,所以不会尝试 4 作为首元素;选择 1 后,下一层从 2 开始,依次得到[1,2]、[1,3]、[1,4],其余分支同理,共得到 6 个组合。
代码实现
import java.util.ArrayList;
import java.util.List;
class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> ans = new ArrayList<>();
backtrack(1, n, k, new ArrayList<>(), ans);
return ans;
}
private void backtrack(int start, int n, int k,
List<Integer> path, List<List<Integer>> ans) {
if (path.size() == k) {
ans.add(new ArrayList<>(path));
return;
}
int need = k - path.size();
for (int num = start; num <= n - need + 1; num++) {
path.add(num);
backtrack(num + 1, n, k, path, ans);
path.remove(path.size() - 1);
}
}
}
func combine(n int, k int) [][]int {
ans := make([][]int, 0)
path := make([]int, 0, k)
var dfs func(int)
dfs = func(start int) {
if len(path) == k {
ans = append(ans, append([]int(nil), path...))
return
}
need := k - len(path)
for num := start; num <= n-need+1; num++ {
path = append(path, num)
dfs(num + 1)
path = path[:len(path)-1]
}
}
dfs(1)
return ans
}
复杂度分析
- 时间复杂度:$O\left(k\binom{n}{k}\right)$。共有 $\binom{n}{k}$ 个答案,每个答案复制
k个元素;这也是输出本身的下界。- 空间复杂度:不计返回结果为 $O(k)$,用于递归栈和当前路径;结果占 $O\left(k\binom{n}{k}\right)$。
关键点总结
start保证路径递增,从搜索结构上消除组合的顺序重复。- 剪枝上界
n-need+1来自“剩余数字必须足够填满路径”,应会现场推导。- 记录答案时要深拷贝;Java 的列表和 Go 的切片都不能直接复用。
- 回溯的三个动作必须成对:选择、递归、撤销。
易错点总结
- 下一层传
start+1而不是num+1:会产生重复数字或重复组合。- 收集答案时不复制
path:后续回溯会覆盖已经保存的结果。- 剪枝上界写成固定的
n-k+1:越往深层仍用旧上界,会漏掉包含末尾数字的组合。- 忘记撤销最后一次选择:不同分支会共享错误的路径状态。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 39. 组合总和 | 中等 | 同一元素可重复选取 |
| 40. 组合总和 II | 中等 | 含重复元素时的同层去重 |
| 216. 组合总和 III | 中等 | 个数与和的双重约束剪枝 |
| 78. 子集 | 中等 | 在每个节点而非叶子收集答案 |
| 46. 全排列 | 中等 | 用 used 标记而非 start 控制 |
| 17. 电话号码的字母组合 | 中等 | 多个候选集合的逐层展开 |
| LCR 080. 组合 | 中等 | 同题换皮的模板默写 |