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

LC39 · Combination Sum · 回溯

组合总和:复用当前数就 dfs(i),不是 i+1

候选无重复,每个数能用多次。递归传同一个 i,才能再拿当前数;start 不回头,[2,3] 和 [3,2] 只会出现一次。

dfs(start, remain):remain==0 则拷贝 path。从 i=start 枚举候选,c>remain 则跳过;把 c 推进 path,递归 dfs(i, remain−c),再弹出。传 i 不是 i+1,允许复用。时间看组合树大小,空间 O(target/min)。

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

这是 LeetCode 39. Combination Sum。互不相同的正整数数组 candidates 和一个目标 target,找出所有和为 target 的组合。同一个数可以选无限次。顺序不同的序列算同一组合。

主例 candidates = [2,3,6,7],target = 7。两组解:[2,2,3] 和 [7]。演示先走到只选了一个 2、remain=5,再跳到 [2,2,3] 收解,再跳到单元素 [7]。

第一直觉是排列式搜索:每次从 0 重新枚举。会得到 [2,3,2]、[3,2,2] 和 [2,2,3] 三份。缺的是起点下标:只从当前 i 往右看,组合按非降下标生成。下面从这块缺口写出 dfs(i) 而不是 dfs(i+1)。

同一下标可再选,更小下标不许回头

题目要组合不是排列。搜索树的每一条根到叶路径是一个候选多重集。remain 减到 0 就收一条;remain 还是正但所有剩余候选都更大,这条路死掉,回溯。

缺两件事。一是复用:选了 2 之后还必须能再选 2,否则主例凑不出 [2,2,3]。递归若写成 dfs(i+1),当前数被跳过,永远只用一次。二是去重:下一层若仍从 0 开始,[2,3] 和 [3,2] 会变成两条。下一层从本次选中的 i 开始,下标不降,同一种组合只走非降序列。

从缺口写 dfs(start, remain)。remain==0,把 path 拷贝进答案(必须拷贝,path 之后还会改)。for i:=start; i<n; i++,c 大于 remain 就 continue。path 推入 c,dfs(i, remain-c),path 弹出。不要在 continue 处 break,除非你先排序并能保证后面更大——未排序时 6 后面可能还有 1。

用手走主例。start=0,remain=7。选 2,path=[2],remain=5,仍从下标 0 搜,演示第一帧。再选 2,path=[2,2],remain=3。再选 2,path=[2,2,2],remain=1,2、3、6、7 都大于 1,死路,弹出到 [2,2]。改选 3,path=[2,2,3],remain=0,记录。演示第二帧。回溯后还会试 [2,3,2] 吗?不会:选完 [2,3] 时 start 已是 3 的下标 1,不能回头拿 2。另一条从 7 起:path=[7],remain=0,记录。演示第三帧。6 留下 remain=1,失败。答案只有两组。

与 LC40 不同:本题候选本来无重复,每个数可复用,不必跳过相邻相等值。与 LC518 不同:这里要列出组合,不是只数个数,所以用搜索而不是那张完全背包。path 必须深拷贝,否则结果里每条都会被改成最后一次回溯后的切片。

i 与 i+1 差在能不能再用自己dfs(i) 允许 path 里继续出现 candidates[i]。dfs(i+1) 是「每个下标只用一次」,那是 LC40 的约束。主例没有 dfs(i),就没有 [2,2,3]。
candidates=[2,3,6,7], target=7
2367target=7
路径 [2]剩余 5
选 2 → remain=5,可复用 2

Go:可复用回溯

solution.goGo
func combinationSum(candidates []int, target int) [][]int {
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 candidates[i] > remain { continue }
path = append(path, candidates[i])
dfs(i, remain-candidates[i])
path = path[:len(path)-1]
}
}
dfs(0, target)
return res
}

1remain==0 先拷贝 path。直接 append(res, path) 会和后续回溯共享底层数组。

2for 从 start 起,下标不回头,[2,3] 与 [3,2] 不会各收一次。

3dfs(i, remain-c) 的 i 不加重。主例选完第一个 2,还能再选 2。

4c>remain 用 continue 而不是 return,后面可能有更小的数。

总结

可复用就递归 dfs(i);start 只往右。主例收到 [2,2,3] 和 [7]。

  • 从 0 反复枚举会产出排列。start 不回头才是组合。
  • dfs(i+1) 会禁止 [2,2,3]。复用当前数必须传 i。
  • 收答案时拷贝 path。LC40 才需要「每个数一次」和跳过重复值。
同族题目
LC40组合总和 IILC77组合LC322零钱兑换(无限硬币 DP)