39. 组合总和
个人主页: > 我不会起名字322 < (欢迎各位大佬莅临😊)
其他栏目: > 技术栈学习笔记 <
其他栏目: > 力扣Hot100题目解析 <
其他栏目: > Go项目学习笔记 <
前言:
写了很多的回溯题,一直感觉好复杂,不明白到底应该怎么管理状态,需要传什么参,每一道题都是当场会了,但是等到下一个dfs算法的题目,又不会写了。但是在写今天这道题目,我突然悟了,原来回溯题我们只需要看三个东西,所有题目都一样!
- 首先我们说DFS到底是干了什么事:DFS 是一种遍历策略;当它不设终点、走遍决策树的所有分支时,就是在穷举。回溯题用 DFS 穷举所有决策路径,并在满足条件处收集答案
- 路径(已做的选择) ,这是回溯题要看的第一个数据,路径里面保存了我们已经选择的元素,可以重复也可以不重复,具体看题目要求
选择路径的时候经常要考虑一个数据,就是startIndex,通过设定startIndex,我们能选择答案是否重复,已经遍历过的元素是否需要重复遍历 - 选择列表(当前可做的选择),这是回溯要看的第二个数据,我们一定要把可以选择的列表想清楚,这样我们才能确定我们的函数要朝着哪种方向前进
- 结束条件(到达决策树的底层),这是回溯要看的第三个数据,把符合答案标准的数据写入结果集
整体的一个模板就是这样的
go
func backtrack(路径, 选择列表) {
if 满足结束条件 {
结果集 = append(结果集, 路径的拷贝)
return
}
for 选择 := 选择列表 {
做选择 // 路径加入该选择
backtrack(路径, 新的选择列表)
撤销选择 // 路径移除该选择
}
}
下面我们来看一道题目深入理解一下
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2], target = 1
输出: []
-
我们对于这样需要保证数字大小顺序的题目,首先要使用快排进行排序
gosort.Ints(candidates)//排序 -
其次,我们要定义三个数据
-
一个是当前已经选择过的元素
gopath := []int{} -
一个是结果集,用来写入结果
goret := [][]int{} -
上面两个都是确定的,每一个题目都需要,最后一个主要看每一个题目不同的询问来自己确定,这里我认为在每一次传参path的时候,我们还要记录一下当前的"和"是多少,用来更方便快捷的判断和target的大小
gocurSum := 0
-
-
有了上面的三个数据,我们现在来套用模板来写这道题目
gofunc backtrack(路径, 选择列表) { 首先是这个大框架,路径肯定要传参path,选择的列表可以用startIndex来确定,startIndex后面的数据就是选择列表 } if 满足结束条件 { 这里的满足条件就是当curSum == target的时候 这里的路径拷贝就是把当前符合答案标准的path值拷贝一份,否则直接写入结果集就是地址,后面修改之后,答案直接变了 结果集 = append(结果集, 路径的拷贝) return } //这里我们要想,如果curSum小于target怎么办呢?显然,继续下面的递归加值即可 //那如果curSum大于target呢?那我们就要直接返回,因为再加下去只可能大,不可能小,所以我们直接返回 if curSum > target { return } for i := startIdnex; i < len(candidates); i++ { 这里我们就从startIndex开始遍历,因为startIdnex前面的已经遍历过了 做选择 // 路径加入该选择 backtrack(路径, 新的选择列表)//选择列表不用改为i+1,因为我们允许数字重复 撤销选择 // 当执行到这里的时候,说明向深处递归已经结束,所以把刚刚写入的数据撤销,换一个数据再递归枚举 } -
因此,我们最后改造的函数就是
godfs = func(path []int, ret [][]int{}, startIndex int) { if curSum == target { temp := append([]int{}, path...) ret = append(ret, temp) return } for i := startIndex; i < len(candidates); i++ { if curSum + candidates[i] > target { //这里为什么我没有写成上面的那种形式呢,其实是做的一个决策剪枝,后面再进行递归也是一直比target大,所以直接返回即可 break } //添加新的元素到路径的同时要给curSum加上 path = append(candidates[i]) curSum += candidates[i] dfs(path, ret, i) //在path删除元素的同时要给curSum减去 path = path[:len(path)-1] curSum -= candidates[i] } }
总结
回过头看,这道题完美印证了前言里的"回溯三要素":
| 三要素 | 在这道题里的体现 |
|---|---|
| 路径 | path,记录当前已选的数字 |
| 选择列表 | startIndex 之后的所有 candidates |
| 结束条件 | curSum == target 时收集答案 |
所谓的"回溯难",难的不是代码,而是一开始没把这三样东西想清楚。一旦想清楚,剩下的就是套模板。
本文是 《算法题目解析系列》 的第 36 篇,本系列将持续更新,每篇都提供清晰的思路与编程语言实现。欢迎关注,第一时间获取更新。如果你有想看的题目,也可以在评论区留言告诉我。