news 2026/7/10 1:47:31

递归加料——回溯算法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
递归加料——回溯算法

回溯算法:从探索到回溯的艺术

算法概述

回溯算法是⼀种经典的递归算法,通常用于解决组合问题、排列问题和搜索问题等。
回溯算法的基本思想:从⼀个初始状态开始,按照⼀定的规则向前搜索,当搜索到某个状态无法前进时,回退到前⼀个状态,再按照其他的规则搜索。回溯算法在搜索过程中维护⼀个状态树,通过遍历状态树来实现对所有可能解的搜索。

回溯算法的流程:“试错”,即在搜索过程中不断地做出选择,如果选择正确,则继续向前搜索;否则,回退到上⼀个状态,重新做出选择。回溯算法通常⽤于解决具有多个解,且每个解都需要搜索才能找到的问题

  1. 选择:做出一个选择
  2. 约束:检查当前选择是否满足条件
  3. 回溯:如果当前路径无效,撤销选择,尝试其他选项

回溯算法的核心思想是:搜索状态树,通过遍历状态树来实现对所有可能解的搜索

算法框架

以下是回溯算法的通用模板:

voidbacktrack(vector<int>&path,vector<int>&choice,...){// 满⾜结束条件if(/* 满⾜结束条件 */){// 将路径添加到结果集中res.push_back(path);return;}// 遍历所有选择for(inti=0;i<choices.size();i++){// 做出选择path.push_back(choices[i]);// 做出当前选择后继续搜索backtrack(path,choices);// 撤销选择}}

其中,path表示当前已经做出的选择,choices表示当前可以做的选择。在回溯算法中,我们需要做出选择,然后递归地调用回溯函数。如果满足结束条件,则将当前路径添加到结果集中;否则,我们需要撤销选择,回到上⼀个状态,然后继续搜索其他的选择。回溯算法的时间复杂度通常较高,因为它需要遍历所有可能的解。但是,回溯算法的空间复杂度较低,因为它只需要维护⼀个状态树。在实际应用中,回溯算法通常需要通过剪枝,记忆搜索方法进行优化,以减少搜索的次数,从而提高算法的效率。

递归与回溯

如果上面比较文绉绉的介绍看的一知半解,那如果从回溯算法的本质就是递归这个视角看对待问题呢?

回溯算法天然适合用递归来实现,因为:

  1. 递归的调用栈自动保存了状态
  2. 递归的返回机制天然实现了"回溯",也就是回退到上⼀个状态,也叫恢复现场

所以只要在基础递归方法上加入“恢复现场”的机制,就可以把这个递归当作回溯算法

初探回溯

题目:验证二叉搜索树

思路:
利用二叉搜索树的中序遍历的结果是一个有序的序列这一特性检验;使用一个全局的变量,访问一个节点时与这个全局变量比较,符合要求则更新这个变量,继续遍历。

使用一个全局变量作为前一节点的值来进行验证的这一操作就可以粗略理解为一种恢复现场

代码实现:

classSolution{longprev=LONG_MIN;//-1,int hold不住public:boolisValidBST(TreeNode*root){if(root==nullptr)returntrue;//一旦出错,直接返回(剪枝)if(!isValidBST(root->left))returnfalse;if(root->val<=prev)returnfalse;prev=root->val;returnisValidBST(root->right);}};

其中,只要发现有一处节点不符合二叉搜索树,即可返回结果,剩余的树也不需要再遍历了,这个操作也就是所谓的剪枝

题目:二叉树的所有路径

思路:
使用深度优先遍历(DFS)求解。使用全局变量将路径以字符串形式存储,从根节点开始遍历,每次遍历时将当前节点的值加⼊到路径中,如果该节点为叶子节点,将路径存储到结果中。否则,将 “->” 加入到路径中并递归遍历该节点的左右子树。

在每一层中,当前路径是不一样的,而当遍历完左子树返回到上一层时,是需要将路径恢复的,但是路径我们通过了递归函数的参数来维护,而递归的返回机制天然实现了"回溯",不需要手动执行,这便是回溯

代码实现:

classSolution{vector<string>ret;string sep="->";public:vector<string>binaryTreePaths(TreeNode*root){string path;dfs(root,path);returnret;}voiddfs(TreeNode*root,string path){path+=to_string(root->val);//递归出口if(root->left==nullptr&&root->right==nullptr){ret.emplace_back(path);return;}path+=sep;//添加到路径//遍历if(root->left)dfs(root->left,path);if(root->right)dfs(root->right,path);}};

以上两题和平常写的递归没有什么区别,但是都含有回溯的影子,不需要我们手动去恢复现场。在使用回溯算法的题型中,只需要再基础递归中加上恢复现场的操作,那么这就是一个回溯算法

回溯算法的应用场景

  1. 组合问题:从n个元素中选k个的所有组合
  2. 排列问题:求序列的所有排列
  3. 子集问题:求集合的所有子集
  4. 棋盘问题:N皇后、数独、八皇后等
  5. 分割问题:分割回文串、IP地址划分
  6. 路径问题:在矩阵中寻找特定路径

回溯例题

题目:全排列


思路:对于穷举这种题型,推荐画决策树,这样不容易数漏,而且后续的回溯写起来很清晰。
用每一个数与所有数进行排列组合;需要注意的有两点:一:对于每一层递归,上一层使用过的元素这一层就不能使用了,所以需要进行剪枝,借助一个全局的bool check数组进行判断元素是否使用过(也就是所谓的记忆化搜索优化);二:每种排列组合使用全局的path进行记录,当返回一层递归时需要手动进行回溯,check数组同样需要在返回时手动回溯

代码实现:

classSolution{vector<vector<int>>ret;//存储结果vector<int>path;//记录组合boolcheck[7];//记忆搜索public:vector<vector<int>>permute(vector<int>&nums){dfs(nums);returnret;}voiddfs(vector<int>&nums){//出口if(path.size()==nums.size()){ret.push_back(path);return;}//探索组合for(inti=0;i<nums.size();++i){//检测是否已使用if(!check[i])//剪枝{check[i]=true;path.push_back(nums[i]);dfs(nums);//重复子问题//回溯check[i]=false;path.pop_back();}}}};

题目:子集

思路:画决策树!这里提供两种决策树的画法

一:对于每个元素有两种选择:1. 不进行任何操作;2. 将其添加至当前状态的集合。在递归时我们需要保证递归结束时当前的状态与进行递归操作前的状态不变,而当我们在选择进行步骤 2 进行递归时,当前状态会发生变化,因此我们需要在递归结束时撤回添加操作,即进行回溯。而且从决策树发现,这里枚举的所有结果都是需要的,也就不需要进行剪枝操作了。


代码实现:

classSolution{vector<vector<int>>ret;vector<int>path;public:vector<vector<int>>subsets(vector<int>&nums){dfs(nums,0);returnret;}voiddfs(vector<int>&nums,intpos){if(pos==nums.size()){ret.push_back(path);return;}// 选path.push_back(nums[pos]);dfs(nums,pos+1);path.pop_back();// 恢复现场// 不选dfs(nums,pos+1);}};

二:将当前元素与后续元素进行组合,这样画出的决策树可以发现每次组合出来的树就是子集,直接保存即可;同样的,这里枚举的所有结果都是需要的,无需进行剪枝操作。


代码实现:

classSolution{vector<vector<int>>ret;//存放结果vector<int>path;//存放路径public:vector<vector<int>>subsets(vector<int>&nums){ret.push_back(path);//处理空集dfs(nums,0);returnret;}voiddfs(vector<int>&nums,intpos){for(inti=pos;i<nums.size();++i){path.push_back(nums[i]);//存放结果ret.push_back(path);//求子集dfs(nums,i+1);//每一层直接得出当前的子集,path.pop_back();//回溯}}};

对于需要穷举所有可能的结果时,建议画决策树;虽说回溯算法有所谓的模板,但是通过上述两题可发现,当你的决策树画出来时,所需要的全局变量也就出来了,是否需要进行剪枝也知道了,什么时候需要回溯也知道了,所以强烈建议画决策树

总结

回溯算法是一种强大而灵活的算法,特别适合解决需要尝试所有可能性的问题。虽然它的时间复杂度可能较高,但通过合理的剪枝和优化,可以显著提高效率。掌握回溯算法需要理解其核心的"选择-约束-回溯"模式,并通过大量练习来培养解决问题的直觉。

关键要点

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/10 12:08:14

37、UNIX系统用户管理与支持全攻略

UNIX系统用户管理与支持全攻略 1. 用小事留下大印象 在与用户的交互中,一些看似微不足道的小事往往能给用户留下深刻的印象。以下是几个具体的方法: 1.1 倾听用户 作为管理员,日常工作总是十分繁忙,任务清单上总有做不完的事情。无论是在走廊上偶遇用户,还是用户打电话…

作者头像 李华
网站建设 2026/7/9 14:09:07

38、UNIX系统用户管理与故障处理全攻略

UNIX系统用户管理与故障处理全攻略 1 UNIX系统中的故障类型及时间预估 在使用UNIX服务器时,故障停机是难以避免的情况,而且要配合用户的使用时间来安排故障处理往往颇具挑战。常见的故障类型有以下三种: - 定期计划故障 :在生产环境中,这些是固定的停机时间,所有用户…

作者头像 李华
网站建设 2026/7/10 6:05:18

39、深入了解 UNIX 系统管理职业:层级、要求与发展路径

深入了解 UNIX 系统管理职业:层级、要求与发展路径 1. 引言 UNIX 系统管理工作涵盖了诸多方面,如求职、应对面试、面试候选人等。同时,还涉及与供应商、销售代表、响应中心、帮助台以及支持工程师的协作。这份工作远不止坐在终端前那么简单,接下来我们将详细了解 UNIX 系…

作者头像 李华
网站建设 2026/7/9 18:08:30

43、UNIX系统管理:招聘、面试与供应商合作指南

UNIX系统管理:招聘、面试与供应商合作指南 1. 寻找合适的UNIX系统管理员 在招聘UNIX系统管理员时,找到符合要求的候选人并非易事。有时,理想的人选可能并不存在,你可能需要降低一些标准来寻找最接近要求的候选人。以下是一些寻找理想候选人的资源: - 报纸 :在主要报…

作者头像 李华
网站建设 2026/7/8 17:01:56

Flutter Packages 设计与实践:构建可维护的模块化应用

引言&#xff1a;为什么 Flutter Packages 如此重要&#xff1f; “在 Flutter 的世界里&#xff0c;Packages 不仅是一种技术&#xff0c;更是一种哲学——它代表着模块化、复用性和社区协作的力量。” 第一部分&#xff1a;Packages 基础概念 1.1 什么是 Flutter Package&…

作者头像 李华
网站建设 2026/7/10 0:05:28

45、UNIX 系统安装、管理与维护全攻略

UNIX 系统安装、管理与维护全攻略 在 UNIX 系统的使用过程中,系统安装、事件记录以及工具使用都是至关重要的环节。下面将为大家详细介绍相关内容,帮助大家更好地管理和维护 UNIX 系统。 1. UNIX 系统安装清单 无论是首次安装操作系统还是重建操作系统,制定一个周全的计划…

作者头像 李华