当前:LC77 · 组合 · 首次出现于 Day 26 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC77 · Combinations · 回溯

组合:从 start 往右数,凑满 k 个就收

1 到 n 里选 k 个,顺序不重要。下一层只从比当前更大的数开始,[2,1] 这种换序枝根本不会长出来。

回溯 dfs(start):从 start 起枚举候选加入 path;当 len(path) == k 时记录路径并返回。剪枝:i 最大只需枚举到 n−(k−len(path))+1,避免无意义扩展。时间 O(C(n,k)·k),空间 O(k)。

时间 O(C(n,k)·k)空间 O(k)结论先行 · 全文约 6 节
导读

给定 n 和 k,返回 [1, 2, …, n] 中所有长度为 k 的组合。组合不管顺序:[1,2] 和 [2,1] 是同一个,答案里只留一份。

主例 n=4,k=2。六种: [1,2]、[1,3]、[1,4]、[2,3]、[2,4]、[3,4]。演示先走 [1] 且下一层从 2 起,收下 [1,2],再改选 3 得到 [1,3],最后六份齐。

LC78 子集没有长度上限,每种长度都收;排列用 used 允许换序。本文要回答:start 怎样单方向避免换序,以及主例为什么选了 1 之后不必再回头选比 1 小的数。

缺口是单向候选,不是 k 层循环

k=2 时写两层 for 能出那六种。k 变成参数,循环层数写不死。若像排列那样每层扫 1..n 再用 used,[1,2] 和 [2,1] 会当两个答案交上去。缺的是「剩下的数只许比已经选的更大」——用一个 start 把候选截成右半截。

dfs(start) 表示:还要往 path 里塞数,且只能从 start、start+1、…、n 里选。path 长度已经是 k,拷贝进答案并返回,不再往下长。否则 for i := start; …,把 i 推进 path,递归 dfs(i+1),回来再 pop。i+1 保证下一层比 i 大,同一组合只按升序生成一次。

还缺一层剪枝。若还要 m = k-len(path) 个数,而 i 已经大到后面剩不够 m 个,再选就是空转。上界写成 i <= n-m+1。主例一开始 m=2,i 只枚举到 3:从 4 起手凑不齐两个。选了 3 之后 m=1,下一层可以选 4,[3,4] 仍在。

用手走主例。dfs(1),path 空。选 1,进入 dfs(2),演示第一帧 path=[1]、start=2。再选 2,path=[1,2],长度到 2,收下,演示第二帧。弹出 2,改选 3,收下 [1,3];再选 4,收下 [1,4]。弹出 1,本层选 2,后面只走 3、4,得到 [2,3]、[2,4]。最后 [3,4]。六份,与终帧一致。

空 path 时不要收集——k≥1 时 [] 不是答案;k=0 的边界题目一般不给。收集必须拷贝。start 只增不减,不会出现 [2,1]。排列题没有这句话,必须用 used。子集题没有「长度到 k 就停」,会继续长到 [1,2,3,4]。

组合 vs 排列组合靠 start 单向,保证升序、不换序。排列靠 used,每个空位都能坐还没坐过的数。主例选 1 之后 start=2,1 不会再出现,[2,1] 长不出来。
n=4, k=2
1234
路径 [1]目标长度 k=2
选 1,从 2 起继续

Go:start 回溯

solution.goGo
func combine(n, k int) [][]int {
res := [][]int{}
path := []int{}
var dfs func(int)
dfs = func(start int) {
if len(path) == k {
res = append(res, append([]int{}, path...))
return
}
for i := start; i <= n-(k-len(path))+1; i++ {
path = append(path, i)
dfs(i + 1)
path = path[:len(path)-1]
}
}
dfs(1)
return res
}

1长度到 k 立刻拷贝返回。主例 [1,2] 不会再去挂 3。

2上界 n-(k-len)+1 是「剩下的数还够不够」。空 path 时 i 只到 3,4 不会当第一项。

3dfs(i+1) 单向。pop 后同层试下一个 i,于是 [1,2] 之后能长出 [1,3]。

4从 1 起,因为数字是 1..n,不是 0..n-1。

总结

start 只往大走,长度到 k 就收。n=4,k=2 六种,没有 [2,1]。

  • 组合靠 start 去换序,排列靠 used 去重用。两套树不要混。
  • 到 k 就停,区别于子集「每层都收」。
  • 剪枝上界让不够长的尾巴提前死掉,答案集合不变。
同族题目
LC78子集LC39组合总和LC46全排列