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

相关推荐
h39741 小时前
DirectShow过滤器开发-视频源过滤器
c++·windows·音视频·directshow
程序猿编码3 小时前
榨干 RTX5090 算力!Qwen3 专用单卡推理引擎,手写C++/CUDA 算子实现 MTP 推测解码
开发语言·c++·大模型推理·qwen3·多模态推理
苍何3 小时前
开源微信流 Windows,微信聊天记录,可以直接给 Codex 和 Obsidan 了
后端
西飘客4 小时前
vs 怎么根据dll 生成lib文件
c++·qt
代码什么用5 小时前
Spring基础使用
java·后端·spring
程序员老陆5 小时前
深入理解 C++ thread_local:线程私有存储的正确打开方式
开发语言·c++·程序设计
信奥卷王5 小时前
[GESP202609 六级] 分树规划
算法
夏幻灵5 小时前
LeetCode 21:合并两个有序链表——双指针与哑节点详解
算法·leetcode·链表
明月_清风5 小时前
Deno 终局来了:从挑战 Node 到被 Cloudflare 收编
前端·后端·node.js