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

news2024/9/17 8:19:56

目录

  • 认识回溯思想
  • 回溯的代码框架
  • 从 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.coloradmin.cn/o/1133093.html

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

相关文章

看我为了水作业速通 opengl freeglut!

参考视频计算机图形学基础–OpenGL的实现_哔哩哔哩_bilibiliT 图形绘制 点 GL_POINTS #define FREEGLUT_STATIC // Define a static library for calling functions #include <GL/freeglut.h> // Include the header filevoid myPoints() { //show three points in sc…

MySQL中大量数据优化方案

文章目录 1 大量数据优化1.1 引言1.2 评估表数据体量1.2.1 表容量1.2.2 磁盘空间1.2.3 实例容量 1.3 出现问题的原因1.4 解决问题1.4.1 数据表分区1.4.1.1 简介1.4.1.2 优缺点1.4.1.2 操作 1.4.2 数据库分表1.4.2.1 简介1.4.2.2 分库分表方案1.4.2.2.1 取模方案1.4.2.2.2 range…

JAVA毕业设计105—基于Java+Springboot+Vue的校园跑腿系统(源码+数据库)

基于JavaSpringbootVue的校园跑腿系统(源码数据库)105 一、系统介绍 本系统前后端分离 本系统分为管理员和用户两个角色 用户&#xff1a; 登录&#xff0c;注册&#xff0c;余额充值&#xff0c;密码修改&#xff0c;发布任务&#xff0c;接受任务&#xff0c;订单管理&…

(多线程)并发编程的三大基础应用——阻塞队列、定时器、线程池【手搓源码】

9.2 阻塞式队列 BlockingQueue<Integer> blockingQueue new LinkedBlockingQueue<Integer>();BlockingQueue<String> queue new LinkedBlockingQueue<>(); // 入队列 queue.put("abc"); // 出队列. 如果没有 put 直接 take, 就会阻塞. St…

IDEA 删除一次性删除所有断点

Ctrl Shift F8 &#xff08;打开“断点”对话框&#xff09; Ctrl A &#xff08;选择所有断点&#xff09; Alt Delete &#xff08;删除选定的断点&#xff09; Enter &#xff08;确认&#xff09;

数字孪生技术:工业数字化转型的引擎

数字孪生是一种将物理实体数字化为虚拟模型的技术&#xff0c;这些虚拟模型与其物理对应物相互关联。这种虚拟模型通常是在数字平台上创建的&#xff0c;它们复制了实际设备、工厂、甚至整个供应链的运作方式。这使工业企业能够实现以下益处&#xff1a; 1. 实时监测和分析 数…

(Java)中的数据类型和变量

文章目录 一、字面常量二、数据类型三、变量1.变量的概念2.语法的格式3.整型变量4.长整型变量5.短整型变量6.字节型变量 四、浮点型变量1.双精度浮点数2.单精度浮点数 五、字符型常量六、布尔型变量七、类型转换1.自动类型转换&#xff08;隐式&#xff09;2.强制类型转换(显式…

【数据结构】数组和字符串(四):特殊矩阵的压缩存储:稀疏矩阵——三元组表

文章目录 4.2.1 矩阵的数组表示4.2.2 特殊矩阵的压缩存储a. 对角矩阵的压缩存储b~c. 三角、对称矩阵的压缩存储d. 稀疏矩阵的压缩存储——三元组表结构体初始化元素设置打印矩阵主函数输出结果代码整合 4.2.1 矩阵的数组表示 【数据结构】数组和字符串&#xff08;一&#xff…

一篇教你学会Ansible

前言 Ansible首次发布于2012年&#xff0c;是一款基于Python开发的自动化运维工具&#xff0c;核心是通过ssh将命令发送执行&#xff0c;它可以帮助管理员在多服务器上进行配置管理和部署。它的工作形式依托模块实现&#xff0c;自己没有批量部署的能力。真正具备批量部署的是…

生产管理中,如何做好生产进度控制?

在生产管理中&#xff0c;我们常常会遇到以下问题&#xff1a; 由于计划不清或者无计划&#xff0c;导致物料进度无法保障&#xff0c;经常出现停工待料的情况。 停工待料导致了生产时间不足&#xff0c;为了赶交货期&#xff0c;只能加班加点。 生产计划并未发挥实际作用&am…

14、Python -- 列表推导式(for表达式)与控制循环

目录 for表达式&#xff08;列表推导式&#xff09;列表推导式的说明使用break跳出循环使用continue忽略本次循环使用return结束函数 列表推导式 使用break跳出循环 使用continue忽略本次循环 for表达式&#xff08;列表推导式&#xff09; for表达式用于利用其他区间、元组、…

哪些车企是前向雷达大客户?国产突围/4D升级进展如何

可穿透尘雾、雨雪、不受恶劣天气影响&#xff0c;唯一能够“全天候全天时”工作&#xff0c;同时在中远距离的物体识别能力&#xff0c;毫米波雷达成为二十几年前豪华车ACC功能的必备传感器。 此后&#xff0c;随着视觉感知技术的不断成熟&#xff0c;尤其是Mobileye、特斯拉等…

强化学习代码实战(3) --- 寻找真我

前言 本文内容来自于南京大学郭宪老师在博文视点学院录制的视频&#xff0c;课程仅9元地址&#xff0c;配套书籍为深入浅出强化学习 编程实战 郭宪地址。 正文 我们发现多臂赌博机执行一个动作之后&#xff0c;无论是选择摇臂1&#xff0c;摇臂2&#xff0c;还是摇臂3之后都会返…

MySQL Join 类型

文章目录 1 Join 类型有哪些2 Inner Join3 Left Join4 Right Join5 Full Join 1 Join 类型有哪些 SQL Join 类型的区别 Inner Join: 左,右表都有的数据Left Join: 左表返回所有的行, 右表没有的补充为 NULLRight Loin: 右表返回所有的行, 左表没有的补充为 NULLFull Outer J…

【会员管理系统】篇二之项目搭建、初始化、安装第三方库

一、项目搭建 1.全局安装vue-cli npm install -g vue/cli查看版本信息 vue -V 2.创建项目 vue create 项目名称 回车 回车 剩余选择如下 之后等待项目创建 最后npm run serve 二、初始化配置 1.更改标题 打开public下的index&#xff0c;将title标签里的改成想要设置的…

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

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

【Redis】redis 十大数据类型 概述

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ redis十大数据类型 一、redis字符串&#xff0…

【Elasticsearch】es脚本编程使用详解

目录 一、es脚本语言介绍 1.1 什么是es脚本 1.2 es脚本支持的语言 1.3 es脚本语言特点 1.4 es脚本使用场景 二、环境准备 2.1 docker搭建es过程 2.1.1 拉取es镜像 2.1.2 启动容器 2.1.3 配置es参数 2.1.4 重启es容器并访问 2.2 docker搭建kibana过程 2.2.1 拉取ki…

crossover23.6闪亮登场发布啦,2023最新功能解析

CrossOver刚刚更新了23.6版本&#xff0c;新增了多款游戏的支持&#xff0c;快来看看你想玩的游戏在不在里面吧。点击这里立即下载最新版CrossOver。 软件介绍 CrossOver 23.6 让Mac可以运行Windows程序的工具 已通过小编安装运行测试 100%可以使用。 CrossOver for Mac 23.…