彻底搞懂回溯算法(例题详解)

目录

什么是回溯算法:

 子集问题:

子集问题II(元素可重复但不可复选): 

 组合问题:

组合问题II(元素可重复但不可复选):

排列问题:

排列问题II(元素可重复但不可复选):


什么是回溯算法:

「回溯是递归的副产品,只要有递归就会有回溯」,所以回溯法也经常和二叉树遍历,深度优先搜索混在一起,因为这两种方式都使用了递归。

详细地说:可以将回溯算法过程理解成一颗多叉树的遍历过程, 回溯法按深度优先策略搜索问题的解空间树。首先从根节点出发搜索解空间树,当算法搜索至解空间树的某一节点时,先利用剪枝函数判断该节点是否可行(即能得到问题的解)。如果不可行,则跳过对该节点为根的子树的搜索,逐层向其祖先节点回溯;否则,进入该子树,继续按深度优先策略搜索。

回溯问题的基本框架:


void backtrack(参数) {if (终止条件) {存放结果;return;}for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {//注意i=0,i=start的区别处理节点;backtrack(路径,选择列表); // 递归  注意(i)和(i++)的区别  回溯,撤销处理结果}
}

多叉树的遍历:

与二叉树的遍历类似,但要遍历所有的子节点:

 public void traverse(TreeNode root){if(root == null){return ;}//前序遍历的位置for(Node child : root.children){traverse(child);}//后序遍历的位置}

 如果将前后续遍历的位置放到for循环里面,与上图的区别在于不遍历根节点(通过后面例题加深理解)

那么有了上面的一定了解后,我们看下例题,具体怎么使用框架

 子集问题:

问题描述:

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

 题目链接:78. 子集 - 力扣(LeetCode)

这是一个典型的回溯问题,首先,我们将它模拟成一颗多叉树(根节点为空):

解决这个问题的关键在于如何遍历这颗多叉树,并且子集不重复 ,在遍历的过程中,我们要将每一个节点都收录到集合中,最后返回这个集合。之后我们只需要让子集不重复就好了,这里我们可以通过设定一个变量start来记录当前走过的位置,使其不断+1来进行迭代,保证不重复,具体实现如下:

class Solution {//定义二维数组res用于存储结果List<List<Integer>> res = new LinkedList<>();//定义路径数组LinkedList<Integer> track = new LinkedList<>(); public List<List<Integer>> subsets(int[] nums) {backtrack(nums,0);return res;}void backtrack(int[] nums,int start){//添加路径数组到结果数组中res.add(new LinkedList<>(track));//for循环遍历数组nums,这里相当于遍历这颗多叉树for(int i = start;i < nums.length;i++){//做选择,将选择添加到路径数组中track.add(nums[i]);//回溯,继续向后遍历backtrack(nums,i + 1);//撤销选择,将选择从路径中删除track.removeLast();}}
}

子集问题II(元素可重复但不可复选): 

题目链接:90. 子集 II - 力扣(LeetCode)

输入输出样例:

这里我们以例一为例:为了区别两个 2 是不同元素,后面我们写作 nums = [1,2,2']

按照之前的思路画出子集的树形结构,显然,两条值相同的相邻树枝会产生重复:

这里,我们需要进行剪枝,如果一个节点有多条值相同的树枝相邻,则只遍历第一条,剩下的都剪掉,不要去遍历: 

体现在代码上,需要先进行排序,让相同的元素靠在一起,如果发现 nums[i] == nums[i-1],则跳过

class Solution {List<List<Integer>> res = new LinkedList<>();LinkedList<Integer> track = new LinkedList<>();public List<List<Integer>> subsetsWithDup(int[] nums) {// 先排序,让相同的元素靠在一起Arrays.sort(nums);backtrack(nums, 0);return res;}void backtrack(int[] nums, int start) {// 前序位置,每个节点的值都是一个子集,将他们收集res.add(new LinkedList<>(track));for (int i = start; i < nums.length; i++) {// 剪枝逻辑,值相同的相邻树枝,只遍历第一条if (i > start && nums[i] == nums[i - 1]) {continue;}//选择操作track.addLast(nums[i]);//回溯backtrack(nums, i + 1);//撤销选择track.removeLast();}}
}

 组合问题:

 题目描述:

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

输入输出:

示例 1:

输入:n = 4, k = 2
输出:
[[2,4],[3,4],[2,3],[1,2],[1,3],[1,4],
]

示例 2:

输入:n = 1, k = 1
输出:[[1]]

题目链接:77. 组合 - 力扣(LeetCode)

还是以 nums = [1,2,3] 为例,刚才让我们求所有子集,就是把所有节点的值都收集起来;现在我们只需要把第 2 层(根节点视为第 0 层)的节点收集起来,就是大小为 2 的所有组合

class Solution {List<List<Integer>> res = new LinkedList<>();// 记录回溯算法的递归路径LinkedList<Integer> track = new LinkedList<>();// 主函数public List<List<Integer>> combine(int n, int k) {backtrack(1, n, k);return res;}void backtrack(int start, int n, int k) {// base caseif (k == track.size()) {// 遍历到了第 k 层,收集当前节点的值res.add(new LinkedList<>(track));return;}// 回溯算法标准框架for (int i = start; i <= n; i++) {// 选择track.addLast(i);// 通过 start 参数控制树枝的遍历,避免产生重复的子集backtrack(i + 1, n, k);// 撤销选择track.removeLast();}}
}

组合问题II(元素可重复但不可复选):

题目描述:

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

题目链接:39. 组合总和 - 力扣(LeetCode)

上述子集问题中,我们通过变量start+1来控制树的生成,也就是剪枝,但是这题可以无限制选用一个数字,那么剪枝也就没有必要了,这里我们保持start不变(树会一直生成),只需改变base case(限制条件)就行了,同时定义一个trackSum来维护路径和:

class Solution {List<List<Integer>> res = new LinkedList<>();// 记录回溯的路径LinkedList<Integer> track = new LinkedList<>();// 记录 track 中的路径和int trackSum = 0;public List<List<Integer>> combinationSum(int[] candidates, int target) {if (candidates.length == 0) {return res;}backtrack(candidates, 0, target);return res;}// 回溯算法主函数void backtrack(int[] nums, int start, int target) {// base case,找到目标和,记录结果if (trackSum == target) {res.add(new LinkedList<>(track));return;}// base case,超过目标和,停止向下遍历if (trackSum > target) {return;}// 回溯算法标准框架for (int i = start; i < nums.length; i++) {// 选择 nums[i]trackSum += nums[i];track.add(nums[i]);// 递归遍历下一层回溯树// 同一元素可重复使用,注意参数backtrack(nums, i, target);// 撤销选择 nums[i]trackSum -= nums[i];track.removeLast();}}
}

排列问题:

题目描述:给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

题目链接:46. 全排列 - 力扣(LeetCode) 

刚才讲的组合/子集问题使用 start 变量保证元素 nums[start] 之后只会出现 nums[start+1..] 中的元素,通过固定元素的相对位置保证不出现重复的子集。但排列问题本身就是让你穷举元素的位置,nums[i] 之后也可以出现 nums[i] 左边的元素,所以之前的那一套玩不转了,需要额外使用 used 数组来标记哪些元素还可以被选择,这就相当于剪枝的作用。将全排列问题模拟成一颗多叉树:

class Solution {List<List<Integer>> res = new LinkedList<>();// 记录回溯算法的递归路径LinkedList<Integer> track = new LinkedList<>();// track 中的元素会被标记为 trueboolean[] used;/* 主函数,输入一组不重复的数字,返回它们的全排列 */public List<List<Integer>> permute(int[] nums) {used = new boolean[nums.length];backtrack(nums);return res;}// 回溯算法核心函数void backtrack(int[] nums) {// base case,到达叶子节点if (track.size() == nums.length) {// 收集叶子节点上的值res.add(new LinkedList(track));return;}// 回溯算法标准框架for (int i = 0; i < nums.length; i++) {// 已经存在 track 中的元素,不能重复选择if (used[i]) {continue;}// 做选择used[i] = true;track.addLast(nums[i]);// 进入下一层回溯树backtrack(nums);// 取消选择track.removeLast();used[i] = false;}}
}

排列问题II(元素可重复但不可复选):

比如输入 nums = [1,2,3],那么这种条件下的全排列共有 3^3 = 27 种:

[[1,1,1],[1,1,2],[1,1,3],[1,2,1],[1,2,2],[1,2,3],[1,3,1],[1,3,2],[1,3,3],[2,1,1],[2,1,2],[2,1,3],[2,2,1],[2,2,2],[2,2,3],[2,3,1],[2,3,2],[2,3,3],[3,1,1],[3,1,2],[3,1,3],[3,2,1],[3,2,2],[3,2,3],[3,3,1],[3,3,2],[3,3,3]
]

标准的全排列算法利用 used 数组进行剪枝,避免重复使用同一个元素。如果允许重复使用元素的话,直接放飞自我,去除所有 used 数组的剪枝逻辑就行了

代码详解:

class Solution {List<List<Integer>> res = new LinkedList<>();LinkedList<Integer> track = new LinkedList<>();public List<List<Integer>> permuteRepeat(int[] nums) {backtrack(nums);return res;}// 回溯算法核心函数void backtrack(int[] nums) {// base case,到达叶子节点if (track.size() == nums.length) {// 收集叶子节点上的值res.add(new LinkedList(track));return;}// 回溯算法标准框架for (int i = 0; i < nums.length; i++) {// 做选择track.add(nums[i]);// 进入下一层回溯树backtrack(nums);// 取消选择track.removeLast();}}
}

最后总结:

回溯算法本质就是个多叉树的遍历问题,关键就是在前序遍历和后序遍历的位置做一些操作,算法框架如下:


void backtrack(参数) {if (终止条件) {存放结果;return;}for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {//注意i=0,i=start的区别处理节点;backtrack(路径,选择列表); // 递归  注意(i)和(i++)的区别  回溯,撤销处理结果}
}

写 backtrack 函数时,需要维护走过的「路径」和当前可以做的「选择列表」,当触发「结束条件」时,将「路径」记入结果集。 最后返回结果集合,得到答案。

结语: 写博客不仅仅是为了分享学习经历,同时这也有利于我巩固自己的知识点,总结该知识点,由于作者水平有限,对文章有任何问题的还请指出,接受大家的批评,让我改进。同时也希望读者们不吝啬你们的点赞+关注+收藏,你们的鼓励是我创作的最大动力!

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

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

相关文章

echarts vue 动画效果的水球图、波浪图教程

1、安装插件 前提是已经安装了echarts&#xff08;我的版本是4.2.1&#xff09; npm install echarts-liquidfill --save 我安装了3.1.0版本的&#xff0c;结果运行时报错"TypeError: wave.ensureState is not a function" 原因&#xff1a;echarts版本和echarts-l…

QT绘图

QPainter paintEvent是Qt中一个非常重要的函数&#xff0c;它是QWidget类的一个事件处理函数&#xff0c;用于处理小部件的绘制事件。当Qt认为小部件需要重绘时&#xff08;例如&#xff0c;窗口首次出现时&#xff0c;大小改变时&#xff0c;或者调用了小部件的update()方法时…

Unity3D学习之Lua热更新解决方案(二)XLua

文章目录 1 XLua概述2 xLua导入和AB包相关准备3 C#调用Lua3.1 Lua解析器3.2 文件加载重定向3.3 Lua解析器管理器3.3.1 重定向AB包内的Lua3.3.2 获得_G大表 3.4 全局变量的获取3.5 全局函数的获取3.5.1 无参无返回3.5.2 有参有返回3.5.3 多返回值3.5.4 变长参数 3.6 List和Dicti…

DFA还原白盒AES密钥

本期内容是关于某app模拟登录的,涉及的知识点比较多,有unidbg补环境及辅助还原算法,ida中的md5以及白盒aes,fart脱壳,frida反调试 本章所有样本及资料均上传到了123云盘 llb资料官方版下载丨最新版下载丨绿色版下载丨APP下载-123云盘 目录 首先抓包 fart脱壳 加密位置定位…

运筹学_1.1.4 线性规划问题-解的概念

1.1.4 线性规划问题-解的概念 一、可行解与最优解二、基的概念三、基变量、基向量&#xff1b;非基变量、非基向量&#xff1b;基解、基可行解&#xff1b;四、最优解与可行解、基可行解的关系五、用例题&#xff08;枚举法&#xff09;巩固基解、基可行解、最优解三个概念1、例…

【JAVA】JDK内置工具之appletviewer

下载java 下载java的时候会先下载Java jdk&#xff0c;Java Development Kit Java开发工具包。 然后会下载jre&#xff0c;也就是Java Runtime Environment Java运行环境。什么是JDK、JRE&#xff1f;_java中的jdk,jre代表什么-CSDN博客 下载之后先找到java下的bin文件&#x…

element-plus 的el-img组件访问oss图片自动拼接前端地址

这是我的组件代码 <el-image style"width: 100px; height: 100px" :src"scope.row.logo" />访问时候 竟然凭借上了前端的地址端口 原来是我的oss服务是使用了域名做cdn加速的 内容分发网络&#xff08;CDN&#xff09;或者服务器配置&#xff0c;可…

【NDK系列】Android tombstone文件分析

文件位置 data/tombstone/tombstone_xx.txt 获取tombstone文件命令&#xff1a; adb shell cp /data/tombstones ./tombstones 触发时机 NDK程序在发生崩溃时&#xff0c;它会在路径/data/tombstones/下产生导致程序crash的文件tombstone_xx&#xff0c;记录了死亡了进程的…

【C++入门】缺省参数 | 函数重载

目录 4.缺省参数 4.1缺省参数的概念 4.2缺省参数分类 4.3声明和定义分离&#xff08;声明使用缺省参数&#xff09; 4.&#x1f40d;声明和定义分离到链接 5.函数重载 5.1函数重载的概念 5.2可执行程序的形成步骤 5.3C支持函数重载的原理—名字修饰(name Mangling) 4.…

180基于matlab的频率切片小波变换程序(FTWT)

基于matlab的频率切片小波变换程序&#xff08;FTWT&#xff09;。从一种新的角度出发&#xff0c;通过自由选择频率切片函数、引进新尺度参数&#xff0c;在频率域实现小波变换&#xff0c;该变换能够很好地刻画信号各成分之间的相对能量关系。此外&#xff0c;频率切片小波变…

YOLOv9有效提点|加入SGE、Ge、Global Context、GAM等几十种注意力机制(四)

专栏介绍&#xff1a;YOLOv9改进系列 | 包含深度学习最新创新&#xff0c;主力高效涨点&#xff01;&#xff01;&#xff01; 一、本文介绍 本文只有代码及注意力模块简介&#xff0c;YOLOv9中的添加教程&#xff1a;可以看这篇文章。 YOLOv9有效提点|加入SE、CBAM、ECA、SimA…

超强预测算法:XGBoost预测模型

目录 往期精彩内容&#xff1a; 多变量特征序列、单序列数据预测实战 前言 1 风速数据预处理与数据集制作 1.1 导入数据 1.2 多变量数据预处理与数据集制作 1.3 单序列数据预处理与数据集制作 2超强模型XGBoost——原理介绍 3 模型评估和对比 3.1 随机森林预测模型 3…