算法练习第20天|回溯算法 77.组合问题 257. 二叉树的所有路径

1.什么是回溯算法?

回溯法也可以叫做回溯搜索法,它是一种搜索的方式。其本质是穷举,穷举所有可能,然后选出我们想要的答案。

2.为什么要有回溯算法?

那么既然回溯法并不高效为什么还要用它呢?

因为有的问题能暴力搜出来就不错了,撑死了再剪枝一下,还没有更高效的解法。比如下面这几类问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等。

 3.如何理解回溯算法?

回溯法解决的问题都可以抽象为树形结构,是的,是所有回溯法的问题都可以抽象为树形结构!

因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。所以回溯和递归是分不开的

递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。

4.回溯算法模板 

类似递归算法的三部曲,回溯算法也有三部曲。

  • 第一步:确认回溯函数的参数及返回值。返回值类型一般为void。但是参数不想二叉树递归那样好确定,所以一般先写逻辑,根据回溯代码逻辑的需要再添加相应参数。回溯函数大致长这样:
void backtracking(参数)
  • 第二步:确认回溯函数的终止条件。既然回溯函数的问题可以等效为树形结构,那么就会遍历树形结构就一定会有终止条件。因此回溯也有终止条件。一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。所以终止条件的伪代码如下:
if (终止条件) {存放结果;return;
}
  • 第三步:确认单层回溯的遍历过程

由于回溯一般是在集合中进行递归搜索,集合的大小构成了树的宽度,递归的深度构成了树的深度。如图所示:

回溯函数遍历过程伪代码如下:

for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {处理节点;backtracking(路径,选择列表); // 递归回溯,撤销处理结果
}

for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。

backtracking这里自己调用自己,实现递归。

 整体框架如下:

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

 力扣题目77.组合

77. 组合 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/combinations/description/

题目描述:

给定两个整数 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]]

思路分析:

最直接的方法就是简单粗暴的双层循环(假设n=4,k=2):

int n = 4;
for (int i = 1; i <= n; i++) {for (int j = i + 1; j <= n; j++) {cout << i << " " << j << endl;}
}

这种k为2时就只用双层循环就行了,但是如果k=50,100?自己要写50层、100层循环就不太现实了。所以这个时候就可以使用回溯了。过程示意如图所示:

图中可以发现n相当于树的宽度,k相当于树的深度

那么如何在这个树上遍历,然后收集到我们要的结果集呢?

图中每次搜索到了叶子节点,就找到了一个结果

相当于只需要把达到叶子节点的结果收集起来,就可以求得 n个数中k个数的组合集合。

回溯解法:

来遍历的过程中,会找到复合要求的子集合,还要有记录这些子集合的结果大集合,所以下定义这两个记录结果的vector:

vector<vector<int>> result;  //用于存放组合的集合
vector<int> path; //用于存放复合条件的当前组合

 下面按照回溯三部曲来进行回溯函数的实现:

  • 第一步,确认回溯函数的参数以及返回值。

        返回值为void,函数的参数为n,k。初次之外,为了更有逻辑的进行穷举,我们再设置一个startIndex,表示本次回溯从【1,2,..., n】的哪里开始遍历元素。所以回溯函数长这样:

void backtracking(int n, int k, int startIndex){
}
  •  第二步,确认回溯的终止条件。根据题意,当记录当前组合的path有了k个元素,即本次回溯就可以终止了,要保存当前结果然后返回。

        具体回溯终止条件长这样: 

//回溯第二步:确认回溯函数的终止条件
if(path.size() == k)  //取得一个k长的组合
{result.push_back(path);return;
}
  •  第三步,确认单层回溯函数的遍历过程,即再回溯中需要做哪些事情?

 根据上述伪代码,需要做三件事:1.在当前层处理节点,即将没遍历过程的元素记录一下;2.递归,从刚刚记录过的元素的下一个元素继续进行该过程,直到条件满足代码返回该递归处;3.弹出刚才最后记录的元素,相当于组合的结果返回来到了上一层的分支处(即通过绿色剪头回溯到示意图中的第二层):

 代码如下:

//我要从startIndex往后开始遍历,将得到的节点元素存放在path中
for(int i = startIndex; i <= n; ++i)
{//处理节点path.push_back(i);//递归回溯函数,开始找下一个元素并添加到path中backtracking(n, k, i+1);//回溯,返回上一层path.pop_back();
}

整体代码如下:

class Solution {
public:vector<vector<int>> result;  //用于存放组合的集合vector<int> path; //用于存放复合条件的组合//回溯第一步:确认回溯函数的参数及返回值,//startIndex用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )void backtracking(int n, int k, int startIndex){//回溯第二步:确认回溯函数的终止条件if(path.size() == k)  //取得一个k长的组合{result.push_back(path);return;}    //回溯第三步:确认单层回溯的遍历过程。//我要从startIndex往后开始遍历,将得到的节点元素存放在path中for(int i = startIndex; i <= n; ++i){//处理节点path.push_back(i);//递归回溯函数,开始找下一个元素并添加到path中backtracking(n, k, i+1);//回溯,返回上一层path.pop_back();}}//力扣提供的接口函数vector<vector<int>> combine(int n, int k) {backtracking(n,k,1);return result;}
};

从上述代码来理解,回溯算法的第三步中的递归,会一直执行,直到满足组合满足要求,然后会逐层进行回溯。

同样的回溯思想也可以解下面这道题。 

257. 二叉树的所有路径

257. 二叉树的所有路径 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/binary-tree-paths/description/

题目描述:

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

叶子节点 是指没有子节点的节点。

示例 1:

输入:root = [1,2,3,null,5]
输出:["1->2->5","1->3"]

示例 2:

输入:root = [1]
输出:["1"]

思路分析:

因为要记录根节点到叶子节点的路径,所以二叉树的遍历方式应该为前序遍历,这样才是正确的路径顺序。遍历和和回溯的过程如下图所示,数字表示先后步骤。

下面我们先使用递归的方式,来做前序遍历,然后在递归中使用回溯

递归+回溯解法:

/*** Definition for a binary tree node.* struct TreeNode {*     int val;*     TreeNode *left;*     TreeNode *right;*     TreeNode() : val(0), left(nullptr), right(nullptr) {}*     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}*     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:void traversal(TreeNode* cur, vector<int>& path, vector<string> & result){//中path.push_back(cur->val);// 递归终止条件:这才到了叶子节点if (cur->left == NULL && cur->right == NULL) {string sPath;for (int i = 0; i < path.size() - 1; i++) {sPath += to_string(path[i]);sPath += "->";}sPath += to_string(path[path.size() - 1]);result.push_back(sPath);return;}if (cur->left) { // 左 traversal(cur->left, path, result);path.pop_back(); // 回溯}if (cur->right) { // 右traversal(cur->right, path, result);path.pop_back(); // 回溯}}vector<string> binaryTreePaths(TreeNode* root) {vector<string> result;  //记录路径的集合vector<int> path;  //记录路径中的元素if (root == nullptr) return result;traversal(root, path, result);return result;}

不使用额外的递归函数的写法:

class Solution {
public:vector<string> result;  //记录路径的集合vector<int> path;  //记录路径中的元素//前序递归第一步:确认递归函数的参数和返回值vector<string> binaryTreePaths(TreeNode* root) {if (root == nullptr) return result;path.push_back(root->val);  //中,先记录一下当前节点元素//递归函数第二步:确认递归终止条件。找到叶子节点才算遍历结束if(root->left == nullptr && root->right == nullptr) {//一旦找到叶子节点,我们就需要打印该路径string singlePath;for(int i = 0; i < path.size()-1; i++)  //从前往后提取路径“1-》2-》3”{singlePath += to_string(path[i]);  //元素数字转字符串singlePath += "->";}singlePath += to_string(path[path.size()-1]); //最后一个元素result.push_back(singlePath);  //记录该路径return result;} //递归第三步:确认单层递归逻辑。处了记录当前节点元素,接下来就是递归遍历左右子树了。//但是为了更方便的生成结果所需的字符串,我们将记录当前节点的步骤放在了函数的开头。//如果我们在这里记录当前节点元素,那么上面的递归终止条件返回的路径结果将会缺少最后一个元素//左if(root->left){binaryTreePaths(root->left); //递归遍历左子树path.pop_back();  //回溯,返回上一层对应的根节点,准备向右子树遍历}//右if(root->right){binaryTreePaths(root->right); //递归遍历右子树path.pop_back();  //回溯}return result;}};

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

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

相关文章

如何获得合适的助听器?

要获得一个合适的助听器&#xff0c;您可以按照以下步骤进行&#xff1a; 1. 咨询专业医生或听力专家&#xff1a;首先&#xff0c;建议您咨询专业的耳鼻喉科医生或听力专家。他们可以通过听力测试来评估您的听力损失程度和类型&#xff0c;并为您提供个性化的建议。 2. 选择信…

【软考】UML中的图之用例图

目录 1. 说明2. 建模2.1 说明2.2 语境建模2.3 需求建模 3. 图示4. 组成部分 1. 说明 1.用例图&#xff08;Use Case Diagram&#xff09;。2.展现了一组用例、参与者&#xff08;Actor&#xff09;以及它们之间的关系。3.用例图通常包括以下的内容&#xff1a;用例、参与者、用…

LabVIEW供热管道泄漏监测与定位

LabVIEW供热管道泄漏监测与定位 在现代城市的基础设施建设中&#xff0c;供热管道系统起着极其重要的作用。然而&#xff0c;管道泄漏问题不仅导致巨大的经济损失&#xff0c;还对公共安全构成威胁。因此&#xff0c;开发一种高效、准确的管道泄漏监测与定位技术显得尤为关键。…

基于Bootstrap 4的企业项目体验服务网站模板

目录 一.前言 二.展示 三.下载链接 一.前言 网页包含以下内容&#xff1a; 页面基本信息&#xff1a;设置页面的字符集、兼容性、视口等元数据。 网站标题和描述&#xff1a;定义了网站的标题为"Benten"&#xff0c;同时也设置了关键词和描述。 CSS样式表链接&a…

「GO基础」在Windows上配置VS Code GO语言开发环境

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

Github copilot我用正版登录授权的,来体验一下吧

Github copilot 市面上的那种可以说是破解的&#xff0c;不是代码补全不稳定&#xff0c;就是chat不稳定&#xff0c;反正就是不怎样&#xff01; 下面是官网正版开通的&#xff0c;欢迎体验15天 体验地址&#xff1a;https://www.bilibili.com/read/cv33696436 这种copilo…

Leetcode-48-旋转图像

题目说明 给定一个 n n 的二维矩阵表示一个图像。 将图像顺时针旋转 90 度。 说明&#xff1a;你必须在原地旋转图像&#xff0c;这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。 示例 1: 给定 matrix [ [1,2,3], [4,5,6], [7,8,9] ], 原地旋转输入…

《Kubernetes部署篇:基于Kylin V10+ARM架构CPU+外部etcd使用containerd部署K8S 1.26.15容器版集群(多主多从)》

总结&#xff1a;整理不易&#xff0c;如果对你有帮助&#xff0c;可否点赞关注一下&#xff1f; 更多详细内容请参考&#xff1a;企业级K8s集群运维实战 1、在当前实验环境中安装K8S1.25.14版本&#xff0c;出现了一个问题&#xff0c;就是在pod中访问百度网站&#xff0c;大…

剑指Offer题目笔记32(拓扑排序)

面试题113&#xff1a; 解决方案&#xff1a; 将课程看成图中的节点&#xff0c;如果两门课程存在先修顺序那么它们在图中对应的节点之间存在一条从先修课程到后修课程的边&#xff0c;因此这是一个有向图。可行的修课序列实际上是图的拓扑排序序列。图中的每条边都是从先修课…

Flask 解决指定端口无法生效问题

问题重现 手动指定的IP端口是app.run(host0.0.0.0, port9304)&#xff0c;但是启动的地址显示的却是http://127.0.0.1:5000。 if __name__ __main__:app.run(host0.0.0.0, port9304)启动地址如下&#xff1a; 解决方案 PyCharm会自动识别出来flask项目&#xff08;即使你…

Hadoop数据压缩

Hadoop数据压缩 Hadoop 数据压缩是一种用于减少存储空间和网络传输成本的技术&#xff0c;通常应用于大数据处理场景。随着数据量的不断增长&#xff0c;对存储和网络带宽的需求也在增加&#xff0c;因此采用数据压缩技术可以有效地减少数据的存储和传输成本&#xff0c;提高数…

吴恩达llama课程笔记:第四课提示词技术

羊驼Llama是当前最流行的开源大模型&#xff0c;其卓越的性能和广泛的应用领域使其成为业界瞩目的焦点。作为一款由Meta AI发布的开放且高效的大型基础语言模型&#xff0c;Llama拥有7B、13B和70B&#xff08;700亿&#xff09;三种版本&#xff0c;满足不同场景和需求。 吴恩…