一天一道力扣Hot100(36):深度优先算法---组合总和

39. 组合总和

个人主页: > 我不会起名字322 < (欢迎各位大佬莅临😊)
其他栏目: > 技术栈学习笔记 <
其他栏目: > 力扣Hot100题目解析 <
其他栏目: > Go项目学习笔记 <




前言:

写了很多的回溯题,一直感觉好复杂,不明白到底应该怎么管理状态,需要传什么参,每一道题都是当场会了,但是等到下一个dfs算法的题目,又不会写了。但是在写今天这道题目,我突然悟了,原来回溯题我们只需要看三个东西,所有题目都一样!

  1. 首先我们说DFS到底是干了什么事:DFS 是一种遍历策略;当它不设终点、走遍决策树的所有分支时,就是在穷举。回溯题用 DFS 穷举所有决策路径,并在满足条件处收集答案
  2. 路径(已做的选择) ,这是回溯题要看的第一个数据,路径里面保存了我们已经选择的元素,可以重复也可以不重复,具体看题目要求
    选择路径的时候经常要考虑一个数据,就是startIndex,通过设定startIndex,我们能选择答案是否重复,已经遍历过的元素是否需要重复遍历
  3. 选择列表(当前可做的选择),这是回溯要看的第二个数据,我们一定要把可以选择的列表想清楚,这样我们才能确定我们的函数要朝着哪种方向前进
  4. 结束条件(到达决策树的底层),这是回溯要看的第三个数据,把符合答案标准的数据写入结果集

整体的一个模板就是这样的

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
输出: []
  • 我们对于这样需要保证数字大小顺序的题目,首先要使用快排进行排序

    go 复制代码
    sort.Ints(candidates)//排序
  • 其次,我们要定义三个数据

    1. 一个是当前已经选择过的元素

      go 复制代码
      path := []int{}
    2. 一个是结果集,用来写入结果

      go 复制代码
      ret := [][]int{}
    3. 上面两个都是确定的,每一个题目都需要,最后一个主要看每一个题目不同的询问来自己确定,这里我认为在每一次传参path的时候,我们还要记录一下当前的"和"是多少,用来更方便快捷的判断和target的大小

      go 复制代码
      curSum := 0
  • 有了上面的三个数据,我们现在来套用模板来写这道题目

    go 复制代码
    func 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,因为我们允许数字重复
        撤销选择        // 当执行到这里的时候,说明向深处递归已经结束,所以把刚刚写入的数据撤销,换一个数据再递归枚举
    }
  • 因此,我们最后改造的函数就是

    go 复制代码
    dfs = 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 篇,本系列将持续更新,每篇都提供清晰的思路与编程语言实现。欢迎关注,第一时间获取更新。如果你有想看的题目,也可以在评论区留言告诉我。

相关推荐
纪念 2291 小时前
c++类和对象(四)
开发语言·c++
回家路上绕了弯1 小时前
AI 编程助手为什么反复读取同一份上下文?如何减少无效重复
后端
Lost of 程序猿1 小时前
用 Quartz.NET 优雅地处理 .NET 定时任务:从选型到 Docker 部署全记录
后端·c#·asp.net
汉克老师1 小时前
GESP2026年9月认证C++六级( 第三部分编程题(2、分树规划))精讲
c++·gesp·小学生·学c++编程
东莞市云毅网络有限公司1 小时前
PDF 表格抽取的三条技术路线对比:规则、库解析与版面识别
python·sqlite·自动化运维·geo·数据监测
学生小羊1 小时前
C++ 初阶 学习博客
c语言·c++·c++与c语言的区别·c++基础学习
雨落在了我的手上1 小时前
Java数据结构(十五):哈希表
数据结构
虚无的纽扣1 小时前
【力扣刷题】第三天:有效三角形的个数、快乐数
算法·排序算法
名字还没想好☜1 小时前
Spring Boot 优雅停机实战:等在途请求处理完再退出,配合 K8s preStop 别丢请求
java·后端·spring