组合总和:复用当前数就 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)。
这是 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]。
Go:可复用回溯
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 才需要「每个数一次」和跳过重复值。