[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.mfbz.cn/a/103731.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Jenkins 安装】

一&#xff1a;安装文件夹准备 在/home/admin 界面下新建三个文件夹&#xff0c;用来安装tomcat、maven 1.打开&#xff0c;/home/admin目录 cd /home/admin 2.新建三个文件夹 mkdir tomcat mkdir maven 二&#xff1a;安装tomcat 1.打开tomcat目录进行tomcat的安装 访问:h…

LSKA(大可分离核注意力):重新思考CNN大核注意力设计

文章目录 摘要1、简介2、相关工作3、方法4、实验5、消融研究6、与最先进方法的比较7、ViTs和CNNs的鲁棒性评估基准比较8、结论 摘要 https://arxiv.org/pdf/2309.01439.pdf 大型可分离核注意力&#xff08;LSKA&#xff09;模块的视觉注意力网络&#xff08;VAN&#xff09;已…

SpringDoc API文档工具集成SpringBoot - Swagger3

1、引言 之前在Spring Boot项目中一直使用的是SpringFox提供的Swagger库&#xff0c;发现已经超过3年没出新版本了&#xff01;SpringDoc是一款可以结合Spring Boot使用的API文档生成工具&#xff0c;基于OpenAPI 3&#xff0c;是一款更好用的Swagger库&#xff01;值得一提的是…

高速下载b站视频的解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

JAVA 链式编程和建造者模式的使用(lombok的使用)

0.说明 0.1 链式编程 链式编程的原理是返回一个this对象&#xff0c;也就是返回对象本身&#xff0c;从而达到链式效果。这样可以减少一些代码量&#xff0c;是java8新增的内容。 此处主要介绍在新建对象使用链式编程更加方便的创建对象。链式编程的一些常见用法可以看这个&a…

C++笔记之初始化二维矩阵的方法

C笔记之初始化二维矩阵的方法 —— 2023年5月20日 上海 code review! 文章目录 C笔记之初始化二维矩阵的方法一.常见方法1. 使用数组2. 使用向量3. 使用数组的动态分配4. 使用嵌套的 std::vector 并使用resize方法5. 初始化固定大小的 std::array 二.C中使用vector初始化二维矩…

CSS3属性详解(一)文本 盒模型中的 box-ssize 属性 处理兼容性问题:私有前缀 边框 背景属性 渐变 前端开发入门笔记(七)

CSS3是用于为HTML文档添加样式和布局的最新版本的层叠样式表&#xff08;Cascading Style Sheets&#xff09;。下面是一些常用的CSS3属性及其详细解释&#xff1a; border-radius&#xff1a;设置元素的边框圆角的半径。可以使用四个值设置四个不同的圆角半径&#xff0c;也可…

基于Java的新闻发布管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09; 代码参考数据库参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&am…

华为云HECS云服务器docker环境下安装nacos

华为云HECS云服务器&#xff0c;安装docker环境&#xff0c;查看如下文章。 华为云HECS安装docker-CSDN博客 一、拉取镜像 docker pull nacos/nacos-server二、宿主机创建挂载目录 执行如下命令&#xff1a; mkdir -p /usr/local/nacos/logs mkdir -p /usr/local/nacos/con…

基于指数分布优化的BP神经网络(分类应用) - 附代码

基于指数分布优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码 文章目录 基于指数分布优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码1.鸢尾花iris数据介绍2.数据集整理3.指数分布优化BP神经网络3.1 BP神经网络参数设置3.2 指数分布算法应用 4.测试结果…

模型保存和加载

1、sklearn模型的保存和加载API from sklearn.externals import joblib 保存&#xff1a;joblib.dump(rf, ‘test.pkl’)加载&#xff1a;estimator joblib.load(‘test.pkl’) 2、决策树的模型保存加载案例 保存&#xff1a; import joblib from sklearn.model_selectio…

OpenCV #以图搜图:感知哈希算法(Perceptual hash algorithm)的原理与实验

1. 介绍 感知哈希算法&#xff08;Perceptual Hash Algorithm&#xff0c;简称pHash&#xff09; 是哈希算法的一种&#xff0c;主要用来做相似图片的搜索工作。 2. 原理 感知哈希算法&#xff08;pHash&#xff09;首先将原图像缩小成一个固定大小的像素图像&#xff0c;然后…

【模式识别】贝叶斯决策模型理论总结

贝叶斯决策模型理论 一、引言二、贝叶斯定理三、先验概率和后验概率3.1 先验概率3.2 后验概率 四、最大后验准则五、最小错误率六、最小化风险七、最小最大决策八、贝叶斯决策建模参考 一、引言 在概率计算中&#xff0c;我们常常遇到这样的一类问题&#xff0c;某事件的发生可…

MobileNetV3

相对重量级网络而言&#xff0c;轻量级网络的特点是参数少、计算量小、推理时间短。更适用于存储空间和功耗受限的场景&#xff0c;例如移动端嵌入式设备等边缘计算设备。因此轻量级网络受到了广泛的关注&#xff0c;其中MobileNet可谓是其中的佼佼者。MobileNetV3经过了V1和V2…

iOS 配置通用链接(Universal Link)服务端和开发者后台都配置好了,还是跳转不到App

目录 一、什么是 Universal Link&#xff1f; 1.背景介绍 2.特点 3.运行机制原理&流程图 二、配置教程 1.第一步&#xff1a;开启 Associated Domains 服务 1.1 开通 Associated Domains 2.第二步&#xff1a;服务器配置 apple-app-site-association&#xff08;AAS…

RISC-V架构——中断处理和中断控制器介绍

1、ARM架构中断机制介绍 本文不是从零开始讲解中断&#xff0c;对于中断的基本知识不再赘述&#xff0c;对中断不是很了解可以先学习ARM中断的文章。参考博客&#xff1a;《ARM架构的外部中断介绍(S5PV210芯片)》&#xff1b; 2、RIAC_V架构的中断控制器架构 &#xff08;1&…

Qt之自定义事件QEvent

在Qt中,自定义事件的步骤大概如下: 1.创建自定义事件,自定义事件需要继承QEvent 2.使用QEvent::registerEventType()注册自定义事件类型,事件的类型需要在 QEvent::User 和 QEvent::MaxUser 范围之间,在QEvent::User之前是预留给系统的事件 3.使用sendEvent() 和 postEv…

【软件安装】Linux系统中安装MySQL数据库服务

这篇文章&#xff0c;主要介绍如何在Linux系统中安装MySQL数据库服务。 目录 一、Linux安装MySQL 1.1、下载MySQL安装包 1.2、解压MySQL安装包 1.3、更改存放目录 1.4、创建用户组和用户 1.5、创建数据目录data 1.6、创建my.cnf配置文件 1.7、初始化数据库 1.8、添加m…

C# 基于腾讯云人脸核身和百度云证件识别技术相结合的 API 实现

目录 腾讯云人脸核身技术 Craneoffice.net 采用的识别方式 1、活体人脸核身(权威库)&#xff1a; 2、活体人脸比对&#xff1a; 3、照片人脸核身(权威库)&#xff1a; 调用成本 百度云身份证识别 调用成本 相关结合点 核心代码 实现调用人脸核身API的示例 实现调用身…

微信小程序设置 wx.showModal 提示框中 确定和取消按钮的颜色

wx官方提供的 showModal 无疑是个非常优秀的选择提示工具 但是 我们还可以让他的颜色更贴近整体的小程序风格 cancelColor 可以改变取消按钮的颜色 confirmColor 则可以控制确定按钮的颜色 参考代码如下 wx.showModal({cancelColor: #0000FF,confirmColor: #45B250,content:…
最新文章