回溯大法总结

前言

本篇博客将分两步来进行,首先谈谈我对回溯法的理解,然后通过若干道题来进行讲解,最后总结

对回溯法的理解

回溯法可以看做蛮力法的升级版,它在解决问题时的每一步都尝试所有可能的选项,最终找出所以可行的方案。回溯法非常适合解决由多个步骤组成的问题,并且每个步骤都有多个选项,在每一步选择了其中一个选项之后,就进入下一步,然后又会面临新的选项,就这样重复选择,直至最终的状态。

用回溯法解决问题的过程可以形象地用一个树形结构表示,求解问题的每个步骤可以看作树中的一个节点。如果在某一步有n个可能的选项,每个选项是树中的一条边,经过这些边就可以到达该节点的个子节点。

在采用回溯法解决问题时如果到达树形结构的叶节点,就找到了问题的一个解。如果希望找到更多的解,那么还可以回溯到它的父节点再次尝试父节点其它的选项。如果父节点所有可能的选项都已经试过,那么再回溯到父节点的父节点以尝试它的其他选项,这样逐层回溯到树的根节点。因此,采用回溯法解决问题的过程实质上是在树形结构中从根节点开始进行深度优先遍历。通常,回溯法的深度优先遍历用递归代码实现。

由于回溯法是在所有选项形成的树上进行深度优先遍历,如果解决问题的步骤比较多或每个步骤都面临多个选项,那么遍历整棵树将需要较多的时间,如果明确知道某些子树没有必要遍历,那么在遍历的时候应该避开这些子树以优化效率。通常将使用回溯法时避免遍历不必要的子树的方法称为剪枝。

用回溯解决集合的组合,排列问题

组合不看重元素顺序,因此两个集合中元素个数相同,各元素个数相同,这两个集合就是一个组合。

排列看重元素顺序,因此两个集合中元素个数相同,各元素个数相同,但是元素顺序不同的话,这两个集合就是两个不同的排列。

所有子集

题目

分析

以集合【1,2】为例,有两个元素,每个元素都面临选和不选两种选择,树形图如下图所示:

本题中生成一个子集,可分为若干步,并且每一步都面临若干选择,这正是采用回溯法的典型场景。

代码

class Solution {
public:vector<vector<int>> vv;vector<int> v;vector<int> cnums;vector<vector<int>> subsets(vector<int>& nums) {cnums=nums;dfs(0);return vv;}void dfs(int pos){if(pos==cnums.size()){vv.push_back(v);return;}dfs(pos+1);v.push_back(cnums[pos]);dfs(pos+1);v.pop_back();}
};

在回溯到父节点时,清除之前相应的修改,即恢复现场。

包含K个元素的组合

题目

分析

集合的一个组合也是一个子集,因此求集合的组合的过程和求子集的过程是一样的,这个题目较钱一道题只是增加了一个限制条件,即只找出包含K个数字的组合,只需要在前一道题的基础上稍加修改即可,就可以找出所有包含K个数字的组合。

代码

class Solution {
public:vector<vector<int>> vv;vector<int> v;vector<vector<int>> combine(int n, int k) {dfs(n,k,1);return vv;}void dfs(int n,int k,int pos){if(v.size()==k){vv.push_back(v);return;}if(pos<=n){dfs(n,k,pos+1);v.push_back(pos);dfs(n,k,pos+1);v.pop_back();}}
};
允许重复选择元素的组合

题目

分析

这个题目仍然是关于组合的,但组合中的一个数字可以出现任意次,可以以不变应万变,用回溯法来解决这个问题。

能够用回溯法解决的问题都能够分成若干步来解决,每一步都面临若干选择。对于从集合中选取数字组成组合的问题而言,集合中有多少个数字,解决这个问题就需要多少步,每一步都从集合中取出一个下标为i的数字,此时面临两个选择。一个选择是跳过这个数字不将该数字添加到组合中,那么这一步实际上什么都不做,接下来处理下标为i+1的数字。另一个选择是将该数字添加到组合中,由于一个数字可以重复在集合中出现,也就是说,下一步可能再次选择同一个数字,因此下一步仍然处理下标为i的数字。

代码

class Solution {
public:vector<vector<int>> vv;vector<int> v;vector<vector<int>> combinationSum(vector<int>& candidates, int target) {helper(candidates,target,0);return vv;}void helper(vector<int>& candidates, int target,int pos){if(target==0){vv.push_back(v);return;}else if(pos<candidates.size() && target>0){helper(candidates,target,pos+1);v.push_back(candidates[pos]);helper(candidates,target-candidates[pos],pos);v.pop_back();}}
};

应用回溯法解决问题时如果有可能应尽可能剪枝以优化时间效率。由于题目明确指出数组中的所有数字都是正整数,因此当组合中已有数字之和已经大于目标值时(即递归函数helper的参数target的值小于0时)就没必要再考虑数组中还没有处理的数字,因为再在组合中添加任意正整数元素之后和会更大,一定找不到新的符合条件的组合,也就没必要再继续尝试,这就是函数helper中else if的条件中补充了一个target大于0的判断条件的原因。

包含重复元素集合的组合

题目

分析

这个题目和之前几个题目与组合相关的题目相比,最大的不同之处在于输入的集合中有重复的数字但输出不得包含重复的组合,如果输入的集合中有重复的数字,不经过特殊处理将产生重复的集合。

避免重复的组合的方法就是当在某一步决定跳过某个值为m的数字时,跳过所有值为m的数字。

为了方便跳过后面所有值相同的数字,可以将集合中的数字排序,将相同的数字放在一起,这样方便比较数字。当决定跳过某个值的数字时,可以按顺序扫描后面的数字,直到找到不同的只为止。

代码

class Solution {
public:vector<vector<int>> vv;vector<int> v;vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {sort(candidates.begin(),candidates.end());helper(candidates,target,0);return vv;}void helper(vector<int>& candidates, int target,int pos){if(target==0){vv.push_back(v);return;}else if(pos<candidates.size() && target>0){int next=pos;while(next<candidates.size() && candidates[next]==candidates[pos]) next++;helper(candidates,target,next);v.push_back(candidates[pos]);helper(candidates,target-candidates[pos],pos+1);v.pop_back();}}
};
没有重复元素集合的全排列

题目

分析

假设集合有n个元素,那么生成一个全排列需要n步,当生成排列的第一个数字时会面临n个选项,即n个数字都有可能成为排列的第1个数字,生成排列的第二个数字会面临n-1个选项,即剩下的n-1个数字都有可能成为排列的第2个数字,以此类推。看起来解决这个问题可以分为n步,每一步都面临若干选项,这就是典型的适用回溯法的场景。

方法一

使用一个bool类型数组来标记是否被访问过,每次填写排列的第I个位置时,都从前往后一次遍历没有被访问过的数字,加入排列。

class Solution {
public:vector<vector<int>> vv;vector<int> v;vector<int> cpnums;bool vis[10];vector<vector<int>> permute(vector<int>& nums) {cpnums=nums;helper(0);return vv;}void helper(int pos){if(pos==cpnums.size()){vv.push_back(v);return;}for(int i=0;i<cpnums.size();i++){if(!vis[i]){v.push_back(cpnums[i]);vis[i]=true;helper(pos+1);vis[i]=false;v.pop_back();}}}
};
方法二

排列和组合不同,排列与元素顺序相关,交换数字能够得到不同的排列,生成全排列的过程,就是交换输入集合中元素的顺序以得到不同的排列。

class Solution {
public:vector<vector<int>> vv;vector<int> cpnums;int n;vector<vector<int>> permute(vector<int>& nums) {cpnums=nums;n=cpnums.size();helper(0);return vv;}void helper(int pos){if(pos==n){vector<int> v;for(int x:cpnums)v.push_back(x);vv.push_back(v);}else{for(int i=pos;i<n;i++){Swap(&cpnums[pos],&cpnums[i]);helper(pos+1);Swap(&cpnums[pos],&cpnums[i]);}}}void Swap(int* a,int* b){int tmp=*a;*a=*b;*b=tmp;}
};
包含重复元素集合的全排列

题目

分析

如果集合中有重复的数字,那么交换集合中重复的数字得到的全排列是同一个全排列,例如交换[1,1,2]中的两个数字1并不能得到新的全排列。

为了便于解决有重复元素会出现相同排列问题,先将数组的元素进行排序。

方法一

易知,以红色区域为根节点的子树应该剪掉,但是以绿色区域为根节点的子树是正确的,那么怎么区分二者那?

通过观察不难发现,绿色区域中,目前已填的元素a与前一个元素相同,且前一个元素已经被访问过了,但是红色区域中,目前已填的元素与前一个元素相同,但是前一个元素没有被访问过,这个点就是突破口。

以nums为例,判断条件为  i>0 && nums[i]=nums[i-1] && !vis[i-1] 

class Solution {
public:vector<vector<int>> vv;vector<int> v;vector<int> cpnums;bool vis[10]={false};int n;vector<vector<int>> permuteUnique(vector<int>& nums) {cpnums=nums;n=cpnums.size();sort(cpnums.begin(),cpnums.end());helper(0);return vv;}void helper(int pos){if(pos==n){vv.push_back(v);return;}for(int i=0;i<n;i++){if(!vis[i]){if(i>0 && cpnums[i]==cpnums[i-1] && !vis[i-1]) continue;v.push_back(cpnums[i]);vis[i]=true;helper(pos+1);vis[i]=false;v.pop_back();}}}
};
方法二
class Solution {
public:vector<vector<int>> vv;int n;vector<int> cpnums;bool vis[10];vector<vector<int>> permuteUnique(vector<int>& nums) {sort(nums.begin(),nums.end());cpnums=nums;n=cpnums.size();helper(0);return vv;}void helper(int pos){if(pos==n){vector<int> v;for(int x:cpnums)v.push_back(x);vv.push_back(v);return;}else{set<int> st;for(int i=pos;i<n;i++){if(!st.count(cpnums[i])){st.emplace(cpnums[i]);Swap(&cpnums[pos],&cpnums[i]);helper(pos+1);Swap(&cpnums[pos],&cpnums[i]);}}}}void Swap(int* a,int* b){int tmp=*a;*a=*b;*b=tmp;}
};

该方法不同于方法一,除了是通过交换不同位置的元素之外,在解决重复元素会出现相同全排列问题上,使用set将已访问的元素进行统计,当与要访问的元素相等的元素已经被访问过,那么访问该元素没问题,但是与要访问的元素相等的元素没有被访问过,那么就会出现相同的全排列,因此这一点就是突破口,其实思想还是和方法一解决的突破点一样。

用回溯法解决其它类型的问题
生成匹配的括号

题目

分析

如果输入n,那么生成的括号组合包含n个左括号和n个右括号。因此生成这样的组合需要2n步,每一步生成一个括号,每一步都面临两个选项,既可能生成左括号又可能生成右括号。由此看来,这个问题很适合用回溯法解决。

在生成括号组合时需要注意每一步都要满足限制条件。第一个限制条件是左括号或右括号的树木不能唱过n个。第二个限制条件是括号的匹配原则,即在任意步骤中已经生成的右括号的数目不能唱过左括号的数目。

代码

class Solution {
public:vector<string> v;string s;vector<string> generateParenthesis(int n) {helper(n,n);return v;}void helper(int left,int right){if(left==0 && right==0){v.push_back(s);return;}if(left>0){s+='(';helper(left-1,right);s.pop_back();}if(right>left){s+=')';helper(left,right-1);s.pop_back();}}
};
分割回文子字符串

题目

分析

当处理到字符创中的某个字符时,如果包括该字符在内后面还有n个字符,那么此时面临n个选项,即分割出长度为1的子字符串(只包含该字符),分割出长度为2的子字符串,以此类推,分割出长度为n的子字符串由于题目要求分割出来的每个子字符串都是回文的,因此需要逐一判断这n个子字符串是不是回文的,只有回文子字符串才是符合条件的分割,分割出一段回文子字符串之后,接着分割后面的字符串。

代码

class Solution {
public:vector<vector<string>> ans;vector<string> v;string cps;int n;vector<vector<string>> partition(string s) {cps=s;n=cps.size();helper(0);return ans;}void helper(int start){if(start==n){ans.push_back(v);return;}for(int i=start;i<n;i++){if(isPalindrome(start,i)){v.push_back(cps.substr(start,i-start+1));helper(i+1);v.pop_back();}}}bool isPalindrome(int begin,int end){while(begin<end){if(cps[begin++]!=cps[end--])return false;}return true;}
};
恢复IP地址

题目

分析

IP地址的特点:一个IP被3个 '.' 字符分割成4段,每段都是从0到255之间的一个数字,另外,除“0”本身外,其他数字不能以‘0’开头。

如果输入的字符串长度为n,由于逐一处理字符串中的每个字符,因此需要n步,并且每一步都面临两个可能的选项,由此可见,适合用回溯法来解决。

代码

class Solution {
public:bool isValidSeg(string seg){return (stoi(seg) <=255) && (seg == "0" || seg[0] !='0');}void helper(string s,int i,int segI,string seg,string ip,vector<string>& result){if(i==s.length() && segI == 3 && isValidSeg(seg))result.push_back(ip+seg);else if(i<s.length() && segI <=3){char ch=s[i];if(isValidSeg(seg+ch))helper(s,i+1,segI,seg+ch,ip,result);if(seg.length()>0 && segI<3)helper(s,i+1,segI+1,string(1,ch),ip+seg+".",result);}}vector<string> restoreIpAddresses(string s) {vector<string> result;helper(s,0,0,"","",result);return result;}
};
总结

如果解决一个问题需要若干步骤,并且在每一步都面临若干选项,那么可以尝试用回溯法解决这个问题。适合用回溯法解决的问题的一个特点是解决这个问题存在多个解,而题目往往要求列出所有的解。

采用回溯法能够解决集合的排列,组合的很多问题,仔细分析这些问题及变种的代码就会发现最终的代码都大同小异,都可以采用递归来实现。递归代码需要先确定递归退出的边界条件,然后逐个处理集合中的元素。对于组合类问题,每个数字都面临两个选项,即添加当前数字到组合中或不添加当前数字到组合中。对于排列问题,一个数字如果后面有n个数字,那么面临n+1个选择,即可以将该数字和它后面的数字(包括它本身)交换。根据这些选项做出选择之后再调用递归函数处理后面的数字。

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

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

相关文章

【Git】版本控制工具——Git介绍及使用

目录 版本控制版本控制系统的主要目标分类小结 分布式版本控制系统——GitGit特点Git与SVN的区别Git的工作机制 Git安装Git 团队协作机制团队内协作跨团队协作远程仓库远程仓库的作用有以下几个方面远程仓库操作流程/团队协作流程 Git分支什么是分支分支的好处 Git的常用命令Gi…

【热门话题】CentOS 常见命令指南

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 CentOS 常见命令指南一、文件与目录操作1. 切换目录2. 查看当前目录3. 列出目录…

SVM影像组学特征

近期做一个影像组学的分类模型 做的是一个胃癌T分期的模型&#xff0c;我刷选统计出一些胃癌区域的特征&#xff0c;如图&#xff1a;有癌症面积、体积等等 下面要做一个SVM&#xff08;支持向量机&#xff09;分类的模型&#xff0c;导入该文件&#xff0c;进行二分类&#x…

MFC密码对话框之间数据传送实例(源码下载)

新建一个login工程项目对话框&#xff0c;主对话框IDD_LOGIN_DIALOG中一个显示按钮IDC_BUTTON1、一个密码按钮IDC_BUTTON2。添加一个密码对话框IDD_DIALOG1&#xff0c;添加类password&#xff0c;在对话框中添加一个编辑框IDC_EDIT1、一个确定按钮IDC_BUTTON1。 程序功能&…

百度集团:AI重构,走到哪了?

内有自家公关一号“自曝”狼性文化&#xff0c;主动制造舆论危机。 外有&#xff0c;OpenAI、谷歌、字节、华为等大模型劲敌扎堆迭代新产品&#xff0c; 强敌环伺。 今天我们要说的是早就从BAT掉队的——百度。 最近&#xff0c;在武汉Aapollo Day 2024上&#xff0c;百度发布了…

抖音小店新规重磅来袭!事关店铺流量!商家的福音来了?

大家好&#xff0c;我是喷火龙。 就在前两天&#xff0c;抖店发布了新规&#xff0c;我给大家总结了一下&#xff0c;无非就是两点。 第一点&#xff1a;保证金下调&#xff0c;一证开多店。 第二点&#xff1a;新品上架破10单&#xff0c;有流量扶持。 咱来细细的解读&…

零基础HTML教程(34)--HTML综合实例

文章目录 1. 背景2. 开发流程2.1 网站功能设计2.2 建立网站目录结构2.3 开发首页2.2 生平简介页2.3 经典诗词页2.4 苏轼图集页2.5 留言板 3. 小结 1. 背景 通过前面33篇文章的学习&#xff0c;我们对HTML有了一个比较全面的了解。 本篇&#xff0c;我们编写一个网站实例&…

Unity在Windows平台播放HEVC/H.265格式视频的底层原理

相关术语、概念 HEVC/H.265 HEVC&#xff08;High Efficiency Video Coding&#xff09;是一种视频压缩标准&#xff0c;也被称为H.265。它是一种高效的视频编码标准&#xff0c;可以提供比之前的标准&#xff08;如H.264&#xff09;更高的压缩率&#xff0c;同时保持较高的…

ssm141餐厅点菜管理系统+vue

餐厅点菜管理系统的设计与实现 摘 要 网络技术和计算机技术发展至今&#xff0c;已经拥有了深厚的理论基础&#xff0c;并在现实中进行了充分运用&#xff0c;尤其是基于计算机运行的软件更是受到各界的关注。加上现在人们已经步入信息时代&#xff0c;所以对于信息的宣传和管…

大数据之Hive函数大全

&#x1f527; Hive函数大全 更多大数据学习资料请关注公众号“大数据领航员"免费领取 一、数学函数 1、取整函数: round 1.函数描述 返回值语法结构功能描述doubleround(double a)返回double类型的整数值部分&#xff08;遵循四舍五入&#xff09; 2.例程 hive>…

自定义RedisTemplate序列化器

大纲 RedisSerializerFastJsonRedisSerializer自定义二进制序列化器总结代码 在《RedisTemplate保存二进制数据的方法》一文中&#xff0c;我们将Java对象通过《使用java.io库序列化Java对象》中介绍的方法转换为二进制数组&#xff0c;然后保存到Redis中。实际可以通过定制Red…

智能化让幼儿园管理更加规范

在各个学龄阶段&#xff0c;幼儿园一向都是家长的教师最为操心的&#xff0c;一方面幼儿园孩子自主才能差&#xff0c;安全问题需求分外注重&#xff0c;另一方面&#xff0c;幼儿园孩子年纪小、缺少必定的认知才能和区分才能&#xff0c;需求加强引导。 那么怎么进步幼儿园孩子…

D60SB120-ASEMI整流桥D60SB120参数、封装、尺寸

编辑&#xff1a;ll D60SB120-ASEMI整流桥D60SB120参数、封装、尺寸 型号&#xff1a;D60SB120 品牌&#xff1a;ASEMI 封装&#xff1a;D-SB 批号&#xff1a;2024 最大重复峰值反向电压&#xff1a;1200V 最大正向平均整流电流(Vdss)&#xff1a;60A 功率(Pd)&#x…

力扣--哈希表13.罗马数字转整数

首先我们可以知道&#xff0c;一个整数&#xff0c;最多由2个罗马数字组成。 思路分析 这个方法能够正确将罗马数字转换为阿拉伯数字的原因在于它遵循了罗马数字的规则&#xff0c;并且对这些规则进行了正确的编码和处理。 罗马数字规则 罗马数字由以下字符组成&#xff1a…

运维笔记.MySQL.基于mysqldump数据备份与恢复

运维专题 MySQL.基于mysqldump数据备份与恢复 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite:http://thispage.tech/Email: [email protected]. Shenzhen ChinaAddress of this article:https://blog.csdn.net/qq_2855…

多微信如何高效管理?一台电脑就能搞定!

对于有多个微信号的人来说&#xff0c;管理这些微信无疑是一道难题。 今天&#xff0c;就给大家分享一个能够让你高效管理多个微信号的神器——个微管理系统&#xff0c;下面&#xff0c;就一起来看看它都有哪些功能吧&#xff01; 1、多号同时登录在线 系统支持多个微信号同…

【综合类型第 39 篇】《我的创作纪念日》成为创作者的第2048天

这是【综合类型第 39 篇】&#xff0c;如果觉得有用的话&#xff0c;欢迎关注专栏。 前言 无意间看了一眼CSDN的私信&#xff0c;提示我 Allen Su &#xff0c;不知不觉今天已经是你成为创作者的 第2048天 啦&#xff0c;为了纪念这一天&#xff0c;我们为您准备了一份专属小…

2. PCI总线基本概念

PCI即Peripheral Componet Interconnect&#xff0c;中文意思是“外围器件互联”&#xff0c;是由PCISIG推出的一种局部并行总线标准。PCI总线是由ISA总线发展而来&#xff0c;是一种同步的独立于处理器的32位或64位局部总线。目前&#xff0c;PCI总线广泛应用于连接显卡&#…

操作抖音小店一直不出单怎么办?只需要做好这两点就可以了!

大家好&#xff0c;我是电商小V 最近很多新手小伙伴来咨询我说自己操作抖音小店&#xff0c;自己的店铺长时间不出单应该怎么办&#xff1f;今天咱们就来详细的说一下&#xff0c; 咱们要清楚的就是自己的店铺不出&#xff0c;只需要咱们做好这两点就可以了&#xff0c; 第一点…

特殊变量笔记2

案例需求 在demo4.sh中循环打印输出所有输入参数, 体验$*与$的区别 实现步骤 编辑demo4.sh脚本文件 # 增加命令: 实现直接输出所有输入后参数 # 增加命令: 使用循环打印输出所有输入参数演示 编辑demo4.sh文件 直接输出所有输入参数, 与循环方式输出所有输入参数(使用双引…