算法的四大思想之一:回溯思想

回溯是最重要的算法思想之一,主要解决一些暴力枚举也搞不定的问题(组合、子集、分割、排列、棋盘等等)。性能并不高,但是那些暴力枚举都无法ko的问题能解出来就可以了🤣。

一、回溯思想

定义

是一个种基于深度优先搜索的思想,在搜索过程中通过剪枝操作来减少搜索空间,从而找到问题的解。

回溯可以理解为递归的拓展,而代码结构又特别像深度遍历N叉树,因此只要知道递归,理解回溯并不难。

模板

回溯比其他思想好理解的一点就是它的递归函数是有模板的,下面是伪代码:

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

关于这个递归函数的重点:

  1. 递归函数一般是没有返回值的

  2. 先写终止条件,一般符合终止条件之后就是要收集结果的时候

  3. 单层搜索。是一个for循环,一般来说这个循环遍历的就是集合中的所有元素。在这个for循环里处理元素

  4. 递归(放递归函数)

  5. 回溯操作(手动撤销)

 

二、从树的遍历开始 

透析一种基于深度优先搜素的思想,首先我们需要回顾一下树的遍历。二叉树中,前序遍历的代码如下:

class TreeNode{int val;TreeNode left;TreeNode right;
}void treeDFS(TreeNode root) {if (root == null)return;System.out.println(root.val);treeDFS(root.left);treeDFS(root.right);  
}

如果是n叉树,就会变成:

class TreeNode{int val;List<TreeNode> nodes;
}public static void treeDFS(TreeNode root) {//递归必须要有终止条件if (root == null){return;}//处理节点System.out.println(root.val);//通过循环,分别遍历N个子树for (int i = 1; i <= nodes.length; i++) {treeDFS("第i个子节点");}
}

因为是n叉树,所以没办法再用leftright表示分支了,这里用了一个List。观察上面的代码是不是和回溯的模板特别像! 

 

三、暴力枚举解决不了的原因 

看一下这道题,LeetCode 77:给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。你可以按 任何顺序 返回答案。

示例 :

  • 输入:n = 4, k = 2

  • 输出:[ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]

从4个数中选择2个:

  • 先取1,则有[1,2],[1,3],[1,4]。

  • 然后取2,因为1已经取过了,不再取,则有[2,3],[2,4]。

  • 再取一个3,因为1和2都取过了,不再取,则有[3,4]。

  • 再取4,因为1,2,3都已经取过了,直接返回null。

  • 所以最终是[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]。

写成代码双层循环轻松搞定:

int n = 4;
for (int i = 1; i <= n; i++) {for (int j = i + 1; j <= n; j++) {System.out.println(i + " " + j);}
}

但是如果n和k变得很大呢?取2个用2个循环,取k个要套多少层循环?显然暴力枚举就不行了,这就是组合问题。 

 

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

继续以LeetCode 77为例子,图示枚举n=4,k=2的情况:

  • 从第一层到第二层的分支有四个,分别表示可以取1,2,3,4。

  • 从左向右取数,取过的数,不再重复取。

  • 第一次取1,集合变为2,3,4 ,因为k为2,我们只需要再取一个数就可以了,分别取2,3,4,得到集合[1,2] [1,3] [1,4]。

以此类推:

 n=5,k=3的情况:

分析规律

图中每次访问到一次叶子节点(红框标记处),就找到一个结果。虽然最后一个是空,但是不影响结果。这相当于只需要把从根节点开始每次选择的内容(分支)达到叶子节点时,将其收集起来就是想要的结果。

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

  • 我们每次选择都是从类似{1,2,3,4},{1,2,3,4,5}这样的序列中一个个选的,这就是局部枚举,而且越往后枚举范围越小。

  • 枚举时,我们就是简单的暴力测试而已,一个个验证,能否满足要求,从上图可以看到,这就是N叉树遍历的过程,因此两者代码也必然很像。

  • 我们再看上图中红色大框起来的部分,这个部分的执行过程与n=4,k=2的处理过程完全一致,很明显这是个可以递归的子结构。

 

为什么要手动撤销(放下前任) 

收集每个结果不是针对叶子结点的,而是针对树枝的,比如最上层我们首先选了1,下层如果选2,结果就是{1,2},如果下层选了3,结果就是{1,3},依次类推。

观察图片,我可以在得到{1,2}之后将2撤掉,再继续取3,这样就得到了{1,3},同理可以得到{1,4},之后当前层就没有了,我们可以将1撤销,继续从最上层取2继续进行。 

 

手动撤销对应的代码操作

先将第一个结果放在临时列表path里,得到第一个结果{1,2}之后就将path里的内容放进结果列表resultList中,之后,将path里的2撤销掉, 继续寻找下一个结果{1.3},然后继续将path放入resultList,然后再撤销继续找。 

 

public List<List<Integer>> combine(int n, int k) {List<List<Integer>> resultList = new ArrayList<>();if (k <= 0 || n < k) {return resultList;}//用户返回结果Deque<Integer> path = new ArrayDeque<>();dfs(n, k, 1, path, res);return res;
}public void dfs(int n, int k, int startIndex, Deque<Integer> path, List<List<Integer>> resultList) {//递归终止条件是:path 的长度等于 kif (path.size() == k) {resultList.add(new ArrayList<>(path));return;}//针对一个结点,遍历可能的搜索起点,其实就是枚举for (int i = startIndex; i <= n; i++) {//向路径变量里添加一个数,就是上图中的一个树枝的值path.addLast(i);//搜索起点要加1是为了缩小范围,下一轮递归做准备,因为不允许出现重复的元素dfs(n, k, i + 1, path, resultList);//递归之后需要做相同操作的逆向操作,具体后面继续解释path.removeLast();}
}

 

五、输出二叉树的所有路径 

LeetCode 257:给你一个二叉树的根节点 root ,按任意顺序,返回所有从根节点到叶子节点的路径。

 

分析:可以得出有几个叶子结点就有几条路径。深度优先搜索就是从根节点开始一直找到叶子结点,我们这里可以先判断当前节点是不是叶子结点,再决定是不是向下走,如果是叶子结点,我们就增加一条路径。

从回溯的角度来看得到第一条路径125之后怎么找到第二条路径13,这里很明显就是先将5撤掉,发现还是不行,再撤掉2,然后接着递归就可以了。

class Solution {List<String> ans = new ArrayList<>();public List<String> binaryTreePaths(TreeNode root) {dfs(root,new ArrayList<>());return ans;}private void dfs(TreeNode root,List<Integer> temp){if(root == null){return ;}temp.add(root.val);//如果是叶子结点记录结果if(root.left == null && root.right == null){ans.add(getPathString(temp));}dfs(root.left,temp);dfs(root.right,temp);temp.remove(temp.size()-1);}//拼接结果private String getPathString(List<Integer> temp){StringBuilder sb = new StringBuilder();sb.append(temp.get(0));for(int i = 1;i < temp.size();++i){sb.append("->").append(temp.get(i));}return sb.toString();}
}

 

六、路径总和的问题

LeetCode 113:给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

分析:我们发现根节点是5,因此只要从左侧或者右侧找到targetSum是17的即可。

  • 看左子树,根值为4,那只要从node(4)的左右子树中找targetSum是13

  • node(11)时,我们需要再找和为2的子链路,显然此时node(7)已经超了,要将node(7)给移除掉,继续访问node(2)。左子树遍历完毕

  • 看根结点的右子树,要找targetSum为17的链路,方式与上面的一致。完整代码就是: 

List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {LinkedList<Integer> path = new LinkedList<>();dfs(root,targetSum,path);return res;
}
public void dfs(TreeNode root,int targetSum,LinkedList<Integer> path){if(root == null){return ;}targetSum -= root.val;path.add(root.val);if(targetSum == 0 && root.left == null && root.right == null){res.add(new LinkedList(path));}dfs(root.left,targetSum,path);dfs(root.right,targetSum,path);path.removeLast();
}

总的来说:

  1. 首先,我们传入一个二叉树的根节点和一个目标和。我们创建一个二维列表 res 用于保存结果。

  2. 接着,我们创建一个 LinkedList 类型的 path 用于保存当前路径。使用深度优先搜索(DFS)的方式遍历二叉树,具体实现在 dfs 函数中。在 dfs 函数中,首先我们判断当前节点是否为空。如果为空,则直接返回。

  3. 然后,我们将目标和减去当前节点值,并将当前节点的值添加到路径中。

  4. 进一步,我们判断是否满足目标和为 0 且当前节点为叶子节点(即没有左右子树)。如果满足条件,我们将当前路径添加到结果列表 res 中。

  5. 接下来,我们分别递归地遍历当前节点的左子树和右子树,注意此时目标和和路径都已经更新。

  6. 最后,我们移除路径中的最后一个值,以便继续向上寻找其他路径。

  7. 最终返回保存有所有符合条件路径的结果列表 res。

 

 

 

 

 

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

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

相关文章

免担心!如果你的处理器不支持TPM 2.0,配置一下就可以安装Windows 11了

这篇文章解释了如何使用Windows注册表编辑器将你的电脑设置为Windows 11,即使你没有支持TPM 2.0的处理器。 如何在不支持的处理器中安装Windows 11 要使你的电脑即使有不受支持的处理器也能安装Windows 11,你需要对Windows注册表进行一些更改。这并不像看上去那么复杂,但也…

YOLOv8改进 | 2023Neck篇 | 利用RepGFPN改进特征融合层(附yaml文件+添加教程)

一、本文介绍 本文给大家带来的改进机制是Damo-YOLO的RepGFPN&#xff08;重参数化泛化特征金字塔网络&#xff09;&#xff0c;利用其优化YOLOv8的Neck部分&#xff0c;可以在不影响计算量的同时大幅度涨点&#xff08;亲测在小目标和大目标检测的数据集上效果均表现良好涨点…

Redis对象——内存回收,对象共享和空转时长

一. 内存回收 因为C语言不具备内存回收功能&#xff0c;所以Redis在自己的对象系统中构建了一个引用计数技术实现内存回收机制。通过这一机制&#xff0c;程序可以通过跟踪对象的引用计数信息&#xff0c;在适当的时候自动释放对象并进行内存回收。 内每一个对象的引用计数信息…

平台工程与 DevOps 和 SRE 有何不同?

在现代软件开发和运营的动态领域中 &#xff0c;平台工程、DevOps 和站点可靠性工程 (SRE) 等术语 经常使用&#xff0c;有时可以互换使用&#xff0c;这常常会导致进入或浏览这些领域的专业人员感到困惑。了解这些概念之间的细微差别对于努力构建强大且可扩展的系统的组织至关…

国产Apple Find My「查找」认证芯片-伦茨科技ST17H6x芯片

深圳市伦茨科技有限公司&#xff08;以下简称“伦茨科技”&#xff09;发布ST17H6x Soc平台。成为继Nordic之后全球第二家取得Apple Find My「查找」认证的芯片厂家&#xff0c;该平台提供可通过Apple Find My认证的Apple查找&#xff08;Find My&#xff09;功能集成解决方案。…

连连看游戏

连通块记忆性递归的综合运用 这里x&#xff0c;y的设置反我平常的习惯&#xff0c;搞得我有点晕 实际上可以一输入就交换x&#xff0c;y的数据的 如果设置y1为全局变量的话会warning&#xff1a; warning: built-in function y1 declared as non-function 所以我改成p和q了…

一些好用的VSCode扩展

可以在扩展这里直接搜索需要的扩展&#xff0c;点击安装即可。 1.Chinese 中文扩展&#xff0c;就是说虽然咱们懂点英语&#xff0c;但还是中文看着方便 2.Auto Rename Tag 当你重命名一个HTML 标签时&#xff0c;会自动重命名与他配对的HTML 标签 当你选择h4这个标签时&…

系列三、DDL

一、DDL 1.1、概述 DDL是英文单词Data Definition Language的缩写&#xff0c;中文意思为数据定义语言&#xff0c;是用来定义数据库对象(数据库&#xff0c;表&#xff0c;字段)的。 1.2、数据库操作 1.2.1、查询所有数据库 show databases; 1.2.2、创建数据库 # 语法 cre…

云原生基础入门概念

文章目录 云原生的概念云原生的关键技术为何选择云原生&#xff1f;云原生的实际应用 当谈及现代软件开发和IT基础架构时&#xff0c;云原生成为了一个备受关注的话题。它代表了一种软件架构和开发方法&#xff0c;旨在充分利用云计算环境的优势&#xff0c;以提高应用程序的可…

【AI美图】第02期效果图,AI人工智能全自动绘画,美图欣赏

今天给大家献上一组最新提示词 参照图生成图像 依据参照图生成新的图像需要掌握一些技巧&#xff0c;以下是一些可能有用的技巧&#xff1a; 观察参照图&#xff1a;在开始生成新图像之前&#xff0c;仔细观察参照图是非常重要的。你需要了解图像的布局、颜色、线条、细节等…

新一代“垫图”神器,IP-Adapter的完整应用解读

导读 不用训练lora&#xff0c;一张图就能实现风格迁移&#xff0c;还支持多图多特征提取&#xff0c;同时强大的拓展能力还可接入动态prompt矩阵、controlnet等等&#xff0c;这就是IP-Adapter&#xff0c;一种全新的“垫图”方式&#xff0c;让你的AIGC之旅更加高效轻松。 …

智慧工地源码(微服务+Java+Springcloud+Vue+MySQL)

智慧工地系统是依托物联网、互联网、AI、可视化建立的大数据管理平台&#xff0c;是一种全新的管理模式&#xff0c;能够实现劳务管理、安全施工、绿色施工的智能化和互联网化。围绕施工现场管理的人、机、料、法、环五大维度&#xff0c;以及施工过程管理的进度、质量、安全三…

Python Socket编程

Python Socket编程 文章目录 Python Socket编程1. 弄懂HTTP、Socket、TCP这几个概念五层网络模型 2. client和server实现通信Socket编程模式指南代码实现 3. socket实现聊天和多用户连接4. socket模拟http请求5. socket使用I/O多路复用模式模拟http请求 1. 弄懂HTTP、Socket、T…

51单片机的外部中断的以及相关寄存器的讲解

中断系统 本文主要涉及8051单片机的中断系统的讲解与使用 其中包括中断相关寄存器的介绍与使用以及外部中断初始化的代码分析。 文章目录 中断系统一、 中断的介绍二、 中断结构及相关寄存器2.1 中断源 2.2 中断请求控制器2.2.1 TCON寄存器2.2.2 SCON寄存器2.2.3 中断允许寄存器…

【每日一题】【12.15】2415.反转二叉树的奇数层

&#x1f525;博客主页&#xff1a; A_SHOWY&#x1f3a5;系列专栏&#xff1a;力扣刷题总结录 数据结构 云计算 数字图像处理 力扣每日一题_ 2415. 反转二叉树的奇数层https://leetcode.cn/problems/reverse-odd-levels-of-binary-tree/ 今天终于碰到了一个mid题目&#x…

数据库常用分库分表方案

为什么需要分库分表 分库分表是因应数据库处理大规模数据时所面临的挑战而出现的解决方案. // 提高性能 单个数据库在数据量增加时容易出现性能瓶颈。分库分表可以减轻单个数据库的负担&#xff0c;提高系统的读写性能和响应速度. // 提高并发能力 大量用户同时访问数据库可能…

N-Channel Trench Power MOSFET FMA30H150SL

FMA30H150SL N-Channel Trench Power MOSFET FMA30H150SL Application &#xff1a;  LCD TV  Notebook  Elevator  Inductive heating  Power tools  Broadband FMA30H150SL Features &#xff1a;  30V,150A  RDS(ON)2.4mΩ (Typ.) VGS 10V …

若依 ruoyi-vue3 集成aj-captcha实现滑块、文字点选验证码

目录 0. 前言0.1 说明 1. 后端部分1.1 添加依赖1.2. 修改 application.yml1.3. 新增 CaptchaRedisService 类1.4. 添加必须文件1.5. 移除不需要的类1.6. 修改登录方法1.7. 新增验证码开关获取接口1.8. 允许匿名访问 2. 前端部分&#xff08;Vue3&#xff09;2.1. 新增依赖 cryp…

“一键调整尺寸,轻松完成视频批量剪辑:批量放大视频尺寸“

你是否曾经遇到过需要批量调整视频尺寸的情况&#xff1f;无论是为了适应不同的播放平台&#xff0c;还是为了满足客户的特定需求&#xff0c;批量调整视频尺寸都是一项繁琐而耗时的工作。但是&#xff0c;现在有一种方法可以让你轻松完成这项任务&#xff0c;那就是使用我们的…

Excel高效办公:文秘与行政办公的智能化革新

&#x1f482; 个人网站:【 海拥】【神级代码资源网站】【办公神器】&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f91f; 代理 IP 推荐&#xff1a;&#x1f449;品易 HTTP 代理 IP &#x1f485; 想寻找共同学习交流的小伙伴&#xff0c…