[Go版]算法通关村第十八关青铜——透析回溯的模版

目录

  • 认识回溯思想
  • 回溯的代码框架
  • 从 N 叉树说起
  • 有的问题暴力搜索也不行
  • 回溯 = 递归 + 局部枚举 + 放下前任
  • Go代码【LeetCode-77. 组合】
  • 回溯热身-再论二叉树的路径问题
    • 题目:二叉树的所有路径
      • Go 代码
    • 题目:路径总和 II
      • Go 代码

回溯是最重要的算法思想之一,主要解决一些暴力枚举也搞不定的问题,比如:组合、分割、子集、排列、棋盘等。从性能角度来看回溯算法的效率并不高,但对于这些暴力都搞不定的算法能出结果就很好了,效率低点没关系。

认识回溯思想

回溯可以视为递归的拓展,很多思想和解法都和递归密切相关。因此学习回溯时,对于递归来分析其特征会理解更深刻。

关于递归和回溯的区别,设想一个场景,某猛男想脱单,现在有两种策略:

  1. 递归策略:先于意中人制造偶遇,然后了解人家的情况,然后约吃饭,有好感后尝试拉手,没有拒绝就表白。
  2. 回溯策略:先统计周围所有的单身女孩,然后一个一个表白,被拒绝就说”我喝醉了“,然后就当啥也没发生,继续找下一个。

其实回溯本质就是这么个过程。

回溯最大的好处:有非常明确的模版,所有的回溯都是一个大框架,因此透传理解回溯的框架是解决一切回溯问题的基础。那么就来分析这个框架。

回溯不是万能的,而且能解决的问题也非常明确,比如:组合、分割、子集、排列、棋盘等,不过这些问题具体处理时又有很多不同,需要具体问题具体分析。

回溯可以理解为递归的拓展,而代码结构又特别像 深度遍历 N 叉树,因此只要知道递归,理解回溯并不难。难在很多人不理解为什么在递归语言之后要有个”撤销“的操作。可以假设一个场景:你谈了个新女朋友,来你家之前,你是否会将你前任的东西赶紧藏起来?回溯也是一样,有些信息是前任的,要先处理掉才能重新开始。

回溯的代码框架

func Backtracking(参数) {if 终止条件 {存放结果return}for 选择本层集合中元素(画成树,就是树节点孩子的大小) {处理节点Backtracking()回溯,撤销处理结果}
}

从 N 叉树说起

先看一下 N 叉树遍历的问题,二叉树的前序遍历,代码如下:

/*** Definition for a binary tree node.* type TreeNode struct {*     Val int*     Left *TreeNode*     Right *TreeNode* }*/
func preorderTraversal(root *TreeNode) []int {ret := make([]int, 0)if root == nil {return ret}ret = append(ret, root.Val)ret = append(ret, preorderTraversal(root.Left)...)ret = append(ret, preorderTraversal(root.Right)...)return ret
}

假如现在是一个三叉、四叉甚至 N 叉树该怎么办呢?很显然这时候就不能用 Left 和 Right 来表示分支了,使用一个切片比较好,就是这样:

/*** Definition for a Node.* type Node struct {*     Val int*     Children []*Node* }*/func preorder(root *Node) []int {ret := make([]int, 0)if root == nil {return ret}ret = append(ret, root.Val)for _, v := range root.Children {ret = append(ret, preorder(v)...)}return ret
}

到这里,有没有发现和上面说的回溯的模版非常像了?是的!非常像!既然很像,那说明两者一定存在某种关系。继续往下看

有的问题暴力搜索也不行

我们说回溯主要解决暴力枚举也解决不了的问题。
看个例子:题目链接:LeetCode-77. 组合
在这里插入图片描述
对于示例1,写成代码很容易,双层循环轻松搞定:

func combine(n int, k int) [][]int {ret := make([][]int, 0)for i:=1; i<=n; i++ {for j:=i+1;j<=n;j++ {arr := []int{i, j}ret = append(ret, arr)}}return ret
}

假如 k 变大,比如 k=3 呢?也可以,三层循环基本搞定:

func combine(n int, k int) [][]int {ret := make([][]int, 0)for i:=1; i<=n; i++ {for j:=i+1;j<=n;j++ {for u:=j+1;u<=n;u++ {arr := []int{i, j, u}ret = append(ret, arr)}}}return ret
}

如果这里的 k=5 呢,甚至 k=50 呢?你需要套多少层循环?甚至告诉你 k 就是一个未知的正整数 k,你怎么写循环呢?这时候已经无能为力了,所以暴力搜索就不行了。

这就是组合类型问题,除此之外 子集、排列、切割、棋盘 等方面都有类似的问题。

回溯 = 递归 + 局部枚举 + 放下前任

继续研究 题目链接:LeetCode-77. 组合 ,图示一下上面自己枚举所有答案的过程。
在这里插入图片描述
每次从集合中选取元素,可选择的范围会逐步收缩,到了取 4 时就直接为空了。

观察树结构,可以发现,每次访问到一次叶子节点(图中绿色框),就找到了一个结果。虽然最后一个是空的,但是不影响结果。这相当于只需要把根节点开始每次选择的内容(分支)达到叶子节点时,将其收集起来就是想要的结果。

元素个数 n 相当于树的宽度(横向),每个结果的元素个数 k 相当于树的深度(纵向)。所以我们说回溯算法就是一纵一横而已。再分析其他规律:

  1. 每次选择都是从类似「1 2 3 4」,「2 3 4」这样的序列中一个个选的,这就是局部枚举,而且越往后枚举范围越小。
  2. 枚举时,就是简单的暴力测试,一个个验证,能否满足要求,从上图可以看到,这就是 N 叉树遍历的过程,因此两者代码必然很像。
  3. 从图可见,每个子树都是个可以递归的子结构。

这样我们就将回溯与 N 叉树完美结合在一起了。

但是,还有一个大问题:回溯一般会有个手动撤销的操作,为什么呢?继续观察上图:

可以发现,收集每个结果不是针对叶子节点,而是针对树枝的,比如最上层首先选了 1, 下层如果选2,结果就是「1 2」,如果下层选了3,结果就是「1 3」,依此类推。现在问题是当得到第一个结果「1 2」之后,怎么得到第二个结果「1 3」呢?

可以发现,可以在得到「1 2」之后将 2 撤销,再继续取3,这样就得到了「1 3」,同理可以得到「1 4」,之后当前层就没有了,可以将 1 撤销,继续从最上层取 2 继续进行。
对应的代码操作:就是先将第一个结果放在临时列表 path 里,得到第一个结果「1 2」之后就将 path 里的内容放进结果列表中,之后,将 path 里的 2 撤销,继续寻找下一个结果「1 3」,然后继续讲 path 放入结果,然后再撤销继续找。

Go代码【LeetCode-77. 组合】

题目链接:LeetCode-77. 组合

func combine(n int, k int) [][]int {ret := make([][]int, 0)if k <= 0 || n < k {return ret}path := make([]int, 0)var dfs func(int)dfs = func(start int) {if len(path) == k {// 关键pathcopy := make([]int, k)copy(pathcopy, path)ret = append(ret, pathcopy)return}for i:=start;i<=n;i++ {path = append(path, i)dfs(i+1)path = path[:len(path)-1]}}dfs(1)return ret
}

回溯热身-再论二叉树的路径问题

题目:二叉树的所有路径

题目链接:LeetCode-257. 二叉树的所有路径
在这里插入图片描述

Go 代码

/*** Definition for a binary tree node.* type TreeNode struct {*     Val int*     Left *TreeNode*     Right *TreeNode* }*/
func binaryTreePaths(root *TreeNode) []string {ret := make([]string, 0)if root == nil {return ret}path := make([]int, 0)var dfs func(*TreeNode)dfs = func(node *TreeNode){if node == nil {return}path = append(path, node.Val)if node.Left == nil && node.Right == nil {ret = append(ret, conv(path))path = path[:len(path)-1]return}dfs(node.Left)dfs(node.Right)path = path[:len(path)-1]}dfs(root)return ret
}
func conv(arr []int) string {length := len(arr)strarr := make([]string, length)for i, v := range arr {strarr[i] = strconv.Itoa(v)}return strings.Join(strarr,"->")
}

对比之前递归方式的写法(没有撤回步骤,不是回溯写法)

/*** Definition for a binary tree node.* type TreeNode struct {*     Val int*     Left *TreeNode*     Right *TreeNode* }*/
func binaryTreePaths(root *TreeNode) (res []string) {if root == nil {return nil}var a func(*TreeNode, string)a = func(node *TreeNode, path string) {if node == nil {return}str := fmt.Sprintf("%d", node.Val)path = path+str// 叶子节点if node.Left == nil && node.Right == nil {res = append(res, path)return}a(node.Left, path+"->")a(node.Right, path+"->")}a(root, "")return
}

题目:路径总和 II

题目链接:LeetCode-113. 路径总和 II
在这里插入图片描述

Go 代码

/*** Definition for a binary tree node.* type TreeNode struct {*     Val int*     Left *TreeNode*     Right *TreeNode* }*/
func pathSum(root *TreeNode, targetSum int) [][]int {ret := make([][]int, 0)if root == nil {return ret}path := make([]int, 0)var dfs func(*TreeNode, int)dfs = func(node *TreeNode, sum int) {if node == nil {return}path = append(path, node.Val)// 叶子节点if node.Left == nil && node.Right == nil {// 路径匹配,加入结果列表if node.Val == sum {pathcopy := make([]int, len(path))copy(pathcopy, path)ret = append(ret, pathcopy)}path = path[:len(path)-1]return}dfs(node.Left, sum-node.Val)dfs(node.Right, sum-node.Val)path = path[:len(path)-1]}dfs(root, targetSum)return ret
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/119248.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

flinksql kafka到mysql累计指标练习

flinksql 累计指标练习 数据流向&#xff1a;kafka ->kafka ->mysql 模拟写数据到kafka topic&#xff1a;wxt中 import com.alibaba.fastjson.JSONObject; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.Produ…

新手入门?初登开发者舞台的你所适合的三大开发工具?

对新手开发者来说&#xff0c;工具的简洁性和实用性和自己的产出直接挂钩&#xff0c;一个好用的工具往往会让编译代码减少很多麻烦&#xff0c;有哪些比较适合的工具&#xff0c;几乎成了每个新人必定会问的问题之一。 针对这些疑惑&#xff0c;今天就来讲讲三大新手型开发工…

多张照片怎么打包发给别人?几个步骤轻松搞定!

在工作和生活中&#xff0c;我们常常需要发送多张照片&#xff0c;因为照片数量较多&#xff0c;打包可以减少发送时间&#xff0c;提高发送效率。那么如何操作呢&#xff1f;有什么好用的软件呢&#xff1f;下面向大家介绍三种常用的软件。 方法一&#xff1a;使用7-zip 1、在…

uni-app打包apk实现自动更新

一、直接复制粘贴就可用(豪横) app.vue文件里写 //app.vue里写 <script>export default {onShow: function() {console.log(App Show)},onHide: function() {console.log(App Hide)},onLaunch: function() {let appVersion uni.getSystemInfo({success: function(e) {ap…

更新电脑显卡驱动的操作方法有哪些?

更新显卡驱动可以有效的提升我们电脑的性能&#xff0c;可以通过设备管理器、显卡驱动软件等方式进行检查驱动是否需要更新&#xff0c;并修复一些电脑上已知的显卡问题。 然而&#xff0c;对于一些不是很懂电脑技术的人员来说&#xff0c;更新电脑显卡驱动是一件比较复杂和混乱…

视频号视频提取小程序,快速下载视频号视频

​视频号提取小程序可以帮助用户方便地从视频号视频平台获取到自己喜欢的视频号内容。通过这个小程序&#xff0c;你可以快速搜索并提取出视频号&#xff0c;并进行相关的操作。 据悉视频下载bot小程序目前已经更名为【提取下载小助手】 使用视频号提取小程序有以下几个步骤&…

1.验证码绕过

1.环境 1.前端验证码 抓包 发到重放器 可重复使用 爆破 总结&#xff0c;前端的验证直接删除验证码即可开始爆破 服务端 3.token 爆破

pycharm远程连接Linux服务器

文章目录 一&#xff1a;说明二&#xff1a;系统三&#xff1a;实现远程连接方式一&#xff1a; 直接连接服务器不使用服务器的虚拟环境步骤一&#xff1a;找到配置服务器的地方步骤二&#xff1a;进行连接配置步骤三&#xff1a;进行项目文件映射操作步骤四&#xff1a;让文件…

如何在群晖Synology+Office实现多人编辑一个文件?

使用群晖Synology Office提升生产力&#xff1a;多人同时编辑一个文件 文章目录 使用群晖Synology Office提升生产力&#xff1a;多人同时编辑一个文件本教程解决的问题是&#xff1a;1. 本地环境配置2. 制作本地分享链接3. 制作公网访问链接4. 公网ip地址访问您的分享相册5. 制…

在Go项目中二次封装Kafka客户端功能

1.摘要 在上一章节中,我利用Docker快速搭建了一个Kafka服务,并测试成功Kafka生产者和消费者功能,本章内容尝试在Go项目中对Kafka服务进行封装调用, 实现从Kafka自动接收消息并消费。 在本文中使用了Kafka的一个高性能开源库Sarama, Sarama是一个遵循MIT许可协议的Apache Kafk…

InstructionGPT

之前是写在[LLM&#xff1a;提示学习Prompt Learning]里的&#xff0c;抽出来单独讲一下。 基本原理 在做下游的任务时&#xff0c;我们发现GPT-3有很强大的能力&#xff0c;但是只要人类说的话不属于GPT-3的范式&#xff0c;他几乎无法理解。例如&#xff0c;我们说把句子A变…

Android Studio Logcat日志VIVO手机显示*号问题

咨询VIVO客服 1、拨盘输入 *#06# 获取串码&#xff0c;发送给客服 2、拨号盘输入*#*#112#*#*-右上角菜单-更多-一键授权 注意不要刷机&#xff0c;恢复出厂设置&#xff0c;手动取消授权哦

【Linux】Linux任务管理与守护进程

Linux任务管理与守护进程 一、任务管理1、进程组概念2、作业概念3、会话概念4、相关操作 二、守护进程1、守护进程的创建2、守护进程的库函数 一、任务管理 1、进程组概念 在Linux中&#xff0c;每个进程除了有一个进程ID之外&#xff0c;还有一个属性是进程组(PGID)&#xff…

CAD迷你看图 mac v4.4.5

CAD迷你看图是一款小巧的DWG文件浏览小工具&#xff0c;支持AutoCAD DWG/DXF等常用图纸文件&#xff0c;可脱离AutoCAD快速浏览DWG图纸&#xff0c;并提供了平移、缩放、全屏等功能。该软件采用独特的云技术&#xff0c;根据不同DWG图纸的需要自动装载相应字体&#xff0c;解决…

木马文件检测系统 毕业设计 JAVA+Vue+SpringBoot+MySQL

项目编号&#xff1a;S041&#xff0c;源码已在 Bilibili 中上架&#xff0c;需要的朋友请自行下载。 https://gf.bilibili.com/item/detail/1104375029为了帮助小白入门 Java&#xff0c;博主录制了本项目配套的《项目手把手启动教程》&#xff0c;希望能给同学们带来帮助。 …

Linux网络编程:IP协议

目录 一. IP协议的功能 二. IP协议报头 2.1 IP报头的格式 2.2 IP报头各部分含义 三. IP报文的分片问题 3.1 什么是分片 3.2 分片的原理 3.3 合并报文 四. 网段划分 4.1 网络号和主机号 4.2 网络号和主机号的划分策略 4.3 特殊的IP地址 4.4 IP地址数量不足问题 五.…

「滚雪球学Java」:Java前序篇(章节汇总)

&#x1f3c6;本文收录于「滚雪球学Java」专栏&#xff0c;专业攻坚指数级提升&#xff0c;助你一臂之力&#xff0c;带你早日登顶&#x1f680;&#xff0c;欢迎大家关注&&收藏&#xff01;持续更新中&#xff0c;up&#xff01;up&#xff01;up&#xff01;&#xf…

竞赛选题 深度学习卷积神经网络垃圾分类系统 - 深度学习 神经网络 图像识别 垃圾分类 算法 小程序

文章目录 0 简介1 背景意义2 数据集3 数据探索4 数据增广(数据集补充)5 垃圾图像分类5.1 迁移学习5.1.1 什么是迁移学习&#xff1f;5.1.2 为什么要迁移学习&#xff1f; 5.2 模型选择5.3 训练环境5.3.1 硬件配置5.3.2 软件配置 5.4 训练过程5.5 模型分类效果(PC端) 6 构建垃圾…

文心一言 VS 讯飞星火 VS chatgpt (121)-- 算法导论10.4 2题

二、用go语言&#xff0c;给定一个n结点的二叉树&#xff0c;写出一个 O(n)时间的递归过程&#xff0c;将该树每个结点的关键字输出。 文心一言&#xff1a; 在Go语言中&#xff0c;你可以使用递归函数来遍历二叉树的所有节点&#xff0c;并输出每个节点的关键字。以下是一个…

OpenLayers入门,OpenLayers从vue的assets资源路径加载geojson文件并解析数据叠加到地图上,以加载世界各国边界为例

专栏目录: OpenLayers入门教程汇总目录 前言 本章以加载世界各国边界的GeoJSON格式数据为例,讲解如何使用OpenLayers从vue的assets资源路径加载geojson文件并解析数据叠加到地图上。 二、依赖和使用 "ol": "^6.15.1"使用npm安装依赖npm install ol@…