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

目录

什么是回溯算法:

 子集问题:

子集问题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.mzph.cn/news/715306.shtml

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

相关文章

leetcode hot100 每日温度

在本题中&#xff0c;我们是通过单调栈来解决的&#xff0c;因为我们采用了栈的数据结构&#xff0c;并且&#xff0c;栈内存储的元素是单调的。 本题我们考虑&#xff0c;将气温数组元素的下标存入栈中&#xff0c;首先初始化要把0放入&#xff0c;0是下标的意思。然后我们拿…

华为HCIP Datacom H12-821 卷4

1.单选题 下面哪些策略或工具不能够应用于 OSPF: A、access-list B、prefix-list C、route- Policy D、as-path filter 正确答案&#xff1a; D 解析&#xff1a; as-path-filter命令用来创建AS路径过滤器&#xff0c;OSPF属于IGP协议&#xff0c;不涉及到AS号。 2.单选题…

【python基础学习05课_for循环以及双重for循环】

FOR循环 一、认识循环-while 1、循环条件不能超出列表长度 当i 1&#xff0c;while i < len(lst1) 时&#xff0c;i 3后, 打印print&#xff08;lst[3]&#xff09;小宋老师&#xff0c; 继续1, i 4, 4不小于 len(lst1)&#xff0c;打破循环。 2、循环条件超出列表长度报错…

latex报错I was expecting a `,‘ or a `}‘的解决办法

解决办法——经过检查在ref22后面缺少一个逗号 总结 当你在使用LaTeX时遇到“I was expecting a , or a }”这样的错误&#xff0c;这通常意味着LaTeX在解析你的代码时&#xff0c;预期在某个位置看到一个逗号&#xff08;,&#xff09;或一个大括号&#xff08;}&#xff09;…

每日一题 2369

2369. 检查数组是否存在有效划分 题目描述&#xff1a; 给你一个下标从 0 开始的整数数组 nums &#xff0c;你必须将数组划分为一个或多个 连续 子数组。 如果获得的这些子数组中每个都能满足下述条件 之一 &#xff0c;则可以称其为数组的一种 有效 划分&#xff1a; 子数…

STM32 串口通信

串口发原理 在stm32每个串口内部有发送寄存器和发送移位寄存器。 当调用HAL_UART_Transmit 时&#xff0c;cpu会将发送的数据放入发送寄存器中。发送移位寄存器会将数据转换成电平的高低&#xff0c;从TX发出。 1、轮询模式配置、发送与接收 轮询模式时cpu会不断检测发送数…

嵌入式中汇编语言的基本实现

大家好&#xff0c;今天给大家分享&#xff0c;GNU汇编的语法。 第一&#xff1a;汇编简介 GNU 汇编语法适用于所有的架构&#xff0c;并不是 ARM 独享的&#xff0c;GNU 汇编由一系列的语句组成&#xff0c; 每行一条语句&#xff0c;每条语句有三个可选部分&#xff0c;如下…

小白学视觉 | 详解遗传算法 GA(Python实现代码)

本文来源公众号“小白学视觉”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;详解遗传算法 GA&#xff08;Python实现代码&#xff09; 转自&#xff1a;机器之心 英文&#xff1a;www.analyticsvidhya.com/blog/2017/07/introduc…

在线上传解压PHP文件代码,压缩/压缩(网站一键打包)支持密码登录

在线上传解压PHP文件代码&#xff0c;压缩/压缩(网站一键打包)支持密码登录 资源宝分享&#xff1a;www.httple.net 如果你没有主机控制面板这个是最好选择&#xff0c;不需要数据库&#xff0c;上传当控制面板使用&#xff0c;无需安装任何扩展&#xff0c;安全高&#xff0c;…

重拾前端基础知识:CSS

重拾前端基础知识&#xff1a;CSS 前言选择器简单选择器属性选择器组合选择器 插入CSS内嵌样式&#xff08;Inline Style&#xff09;内部样式&#xff08;Internal Style&#xff09;外部样式&#xff08;External Style&#xff09; 层叠颜色背景颜色文本颜色RGB 颜色HEX 颜色…

ESD管 uClamp3331ZA、AZ5A83-01B 、AZ8523-01B国产替代ESD0321CW

上海雷卯ESD二极管 ESD0321CW替代国外品牌型号uClamp3331ZA、AZ5A83-01B 、AZ8523-01B&#xff0c;参数对比如下&#xff1a; 判断ESD二极管是否可以替代需注意的几点&#xff1a; 1. VRWM 是否接近 2. 抗静电能力是否接近&#xff1b; 3. VBR 是否接近&#xff1b; 4. IPP…

Julia语言中的位运算符、赋值运算符、算术运算符

算术运算符 # 使用基本的赋值运算符 a 10 println("a 的初始值是: $a") # 使用加法赋值运算符 a 5 println("a 加上 5 后的值是: $a") # 使用减法赋值运算符 - a - 3 println("a 减去 3 后的值是: $a") # 使用乘法赋值运算符…

Mistral发布语言大模型Mistral Large;法国新星Mistral挑战 OpenAI 霸主地位

&#x1f989; AI新闻 &#x1f680; Mistral发布语言大模型Mistral Large 摘要&#xff1a;Mistral Large 是 Mistral AI 公司最新发布的旗舰语言模型&#xff0c;具备顶尖水平的推理能力。它主要被设计用于处理复杂的多语言推理任务&#xff0c;比如文本理解、转换和代码生…

上位机图像处理和嵌入式模块部署(上、下位机通信的三个注意点)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 如果最终部署在客户现场的是一个嵌入式设备&#xff0c;那么上位机在做好了算法编辑和算法部署之后&#xff0c;很重要的一步就是处理上位机和下位…

beets,一个有趣的 Python 音乐信息管理工具!

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站AI学习网站。 目录 前言 什么是Beet库&#xff1f; 安装Beet库 使用Beet库 Beet库的功能特性 1. 多种音乐格式支持 2. 自动标签识…

【学习笔记】数据结构与算法05:树、层序遍历、深度优先搜索、二叉搜索树

知识出处&#xff1a;Hello算法&#xff1a;https://www.hello-algo.com/ 文章目录 2.4 树2.4.1 「二叉树 binary tree」2.4.1.1 二叉树基本操作2.4.1.2 二叉树的常见类型「完美二叉树 perfect binary tree」「完全二叉树 complete binary tree」「完满二叉树 full binary tre…

H12-821_106

106.如图所示&#xff0c;RTA的GEO/0/0、GEO/0/1接口分别连接部门1和2&#xff0c;其网段分别为10.1.2.0/24、10.1.3.0/24网段&#xff0c;为限制部门1和2之间的相互访间&#xff0c;在RTA上部署traffic-filter&#xff0c;以下哪些部署方式是正确&#xff1f; A.配置ACL3000拒…

职场的过早优化

过早优化&#xff0c;指的是还没弄清楚需求未来的变化的走向的时候&#xff0c;忽略了更重要的问题。 放在职业发展上&#xff1a;你在没有积累足够职场资源&#xff08;眼界、能力、人脉等等&#xff09;&#xff0c;也没有对职业发展形成清晰认知的时候&#xff0c;就过早地进…

【c++】构造函数(下)——初始化列表

Hello,everybody!构造函数的内容比较多&#xff0c;语法还有些复杂。我分成了两篇文章进行讲解&#xff0c;大家在看过构造函数(上)后再来看这篇文章更容易理解哟&#xff01; 1.初始化列表的格式 类似这种格式&#xff0c;在初始化列表中第一行用冒号开头&#xff0c;剩下的用…

力扣每日一题 使二叉树所有路径值相等的最小代价 满二叉树 贪心

Problem: 2673. 使二叉树所有路径值相等的最小代价 文章目录 思路复杂度Code 思路 &#x1f468;‍&#x1f3eb; 灵神题解 复杂度 ⏰ 时间复杂度: O ( n ) O(n) O(n) &#x1f30e; 空间复杂度: O ( 1 ) O(1) O(1) Code class Solution {public int minIncrements(int …