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

LC40 · Combination Sum II · 回溯 / 去重

组合总和 II:同层去重,递归走 i+1

每个下标在一条组合里最多用一次,所以下一层从 i+1 开始。相同的值排在一起后,同一层只让第一个上场,避免两份一模一样的答案。

先排序。回溯 dfs(start, remain):i > start 且与前一个值相同则跳过(同层去重);选中后递归 dfs(i+1, remain−值)(不复用下标)。remain 为 0 时记录路径。时间 O(2^n),空间 O(n)。

时间 O(2^n)空间 O(n)结论先行 · 全文约 6 节
导读

给你一组候选整数和一个目标 target。每个候选的下标在一条组合里只能用一次;候选里可能有相同的值。找出所有加起来等于 target 的组合。组合内顺序不重要,相同的组合只交一份。

主例 candidates = [10, 1, 2, 7, 6, 1, 5],target = 8。排序后是 [1, 1, 2, 5, 6, 7, 10]。四组答案:[1, 1, 6]、[1, 2, 5]、[1, 7]、[2, 6]。两个 1 来自不同下标,可以同时出现在 [1, 1, 6] 里;但不能交两份 [1, 7]。

和 LC39 比,差在两处:39 每个数能用无限次,递归仍从 i 开始;本题用过的下标不能再用,递归从 i+1 开始,并且要处理重复值。本文要回答:同层跳过和跨层留下为什么不冲突,以及主例那份 [1, 7] 是怎样避开第二份重复的。

传 i+1 管下标,同层跳过管取值

第一直觉是按 39 的写法,选完仍从 i 继续。主例里 1 会被反复加,出现 [1,1,1,1,1,1,1,1] 这种题目不允许的组合。改成每个数只用一次之后,若不去重,两个值为 1 的下标会在第一层各走一遍,交上两份 [1, 7]。结果重复,判题只认一份。

缺的是把「下标不复用」和「取值不重复」拆开。下标不复用:选了位置 i,下一层起点改成 i+1,这个位置再也选不到。取值不重复:先排序,相同的值挨在一起;在同一层的 for 循环里,若 i 不是本层起点、且和前一个值相同,就 continue。本层第一个 1 负责所有「以 1 开头」的分支,后面那个 1 不再当本层的新开头。

「同层」指的是同一次 for 循环、同一个 start。不同深度不算同层。第一层选了下标 0 的 1,走进下一层之后,下标 1 的 1 仍然可以选——那是更深一层,两个 1 叠成 [1, 1, …],这是题目允许的。口诀是 i > start 且值等于前一个才跳,不是看见相同值就一律跳。

用手走主例。排序后 [1, 1, 2, 5, 6, 7, 10],从 start=0、remain=8 开始。第一层选下标 0 的 1,path=[1],remain=7,下一层 start=1,这是演示第一帧。回到第一层再走到下标 1,值还是 1,此时 i>start,跳过,避免再长出一组和「第一个 1 开头」重复的答案;演示把 skipped 标在下标 1。

沿着第一个 1 往下可以选 7:path=[1, 7],remain=0,记下一组。也可以在更深一层再选第二个 1,再接 6,得到 [1, 1, 6]。两层深度不同,所以这两个 1 能同时留下。

其余两组同样来自不同开头:第一层选 2,再接 6,得 [2, 6];第一个 1 后面接 2 再接 5,得 [1, 2, 5]。10 比剩余大,排序后可以直接 break,后面更大的数不必看。收尾四组 [[1,1,6], [1,2,5], [1,7], [2,6]],没有第二份 [1,7]。

选完必须把 path 弹出,回到本层再试下一个 i,否则后面的分支会背着旧数。remain 减到负数的路径不必走:已排序时,当前值已经大于 remain,后面更大,break 即可。这和 90 子集 II、47 全排列 II 是同一句同层去重,只是本题还要盯着剩余和。

同层去重i > start 且与前一个相等才 continue。start 是本层第一个可选项。跨层的相同值是不同下标,主例 [1,1,6] 必须留下。和 LC90 同一句口诀,和 LC39 的差别是递归传 i+1 而不是 i。
candidates 排序后 [1,1,2,5,6,7,10], target=8
11256710target=8
路径 [1]剩余 7
层起点选第一个 1

Go:排序 + 同层去重 + i+1

solution.goGo
func combinationSum2(candidates []int, target int) [][]int {
sort.Ints(candidates)
res := [][]int{}
path := []int{}
var dfs func(int, int)
dfs = func(start, remain int) {
if remain == 0 {
res = append(res, append([]int{}, path...))
return
}
for i := start; i < len(candidates); i++ {
if i > start && candidates[i] == candidates[i-1] { continue }
if candidates[i] > remain { break }
path = append(path, candidates[i])
dfs(i+1, remain-candidates[i])
path = path[:len(path)-1]
}
}
dfs(0, target)
return res
}

1排序是同层去重和 break 剪枝的前提。不排序,相等值不相邻,那句 continue 会漏。

2i > start 才跳:本层第一个相同值要走。主例第一层第二个 1 被跳过,所以不会有两份 [1,7]。

3dfs(i+1) 保证下标不复用。记录答案时必须拷贝 path,否则后续弹出会改掉已经收下的切片。

总结

排序后同层相同值只留第一个,递归传 i+1。主例四组,没有第二份 [1,7]。

  • i+1 管「这个下标只用一次」。写成 i 会变成 39,1 能加到爆。
  • i>start 且值相同才跳。跨层两个 1 可以共存,所以有 [1,1,6]。
  • 已排序时当前值大于剩余即可 break。记录前拷贝 path。
同族题目
LC39组合总和LC90子集 IILC47全排列 II