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

2024-04-19 10:20

本文主要是介绍算法练习第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;}};

这篇关于算法练习第20天|回溯算法 77.组合问题 257. 二叉树的所有路径的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/917268

相关文章

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.

SpringBoot启动报错的11个高频问题排查与解决终极指南

《SpringBoot启动报错的11个高频问题排查与解决终极指南》这篇文章主要为大家详细介绍了SpringBoot启动报错的11个高频问题的排查与解决,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一... 目录1. 依赖冲突:NoSuchMethodError 的终极解法2. Bean注入失败:No qu

MySQL新增字段后Java实体未更新的潜在问题与解决方案

《MySQL新增字段后Java实体未更新的潜在问题与解决方案》在Java+MySQL的开发中,我们通常使用ORM框架来映射数据库表与Java对象,但有时候,数据库表结构变更(如新增字段)后,开发人员可... 目录引言1. 问题背景:数据库与 Java 实体不同步1.1 常见场景1.2 示例代码2. 不同操作

Java时间轮调度算法的代码实现

《Java时间轮调度算法的代码实现》时间轮是一种高效的定时调度算法,主要用于管理延时任务或周期性任务,它通过一个环形数组(时间轮)和指针来实现,将大量定时任务分摊到固定的时间槽中,极大地降低了时间复杂... 目录1、简述2、时间轮的原理3. 时间轮的实现步骤3.1 定义时间槽3.2 定义时间轮3.3 使用时

Python实现将MySQL中所有表的数据都导出为CSV文件并压缩

《Python实现将MySQL中所有表的数据都导出为CSV文件并压缩》这篇文章主要为大家详细介绍了如何使用Python将MySQL数据库中所有表的数据都导出为CSV文件到一个目录,并压缩为zip文件到... python将mysql数据库中所有表的数据都导出为CSV文件到一个目录,并压缩为zip文件到另一个

如何解决mysql出现Incorrect string value for column ‘表项‘ at row 1错误问题

《如何解决mysql出现Incorrectstringvalueforcolumn‘表项‘atrow1错误问题》:本文主要介绍如何解决mysql出现Incorrectstringv... 目录mysql出现Incorrect string value for column ‘表项‘ at row 1错误报错

如何解决Spring MVC中响应乱码问题

《如何解决SpringMVC中响应乱码问题》:本文主要介绍如何解决SpringMVC中响应乱码问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Spring MVC最新响应中乱码解决方式以前的解决办法这是比较通用的一种方法总结Spring MVC最新响应中乱码解

利用Go语言开发文件操作工具轻松处理所有文件

《利用Go语言开发文件操作工具轻松处理所有文件》在后端开发中,文件操作是一个非常常见但又容易出错的场景,本文小编要向大家介绍一个强大的Go语言文件操作工具库,它能帮你轻松处理各种文件操作场景... 目录为什么需要这个工具?核心功能详解1. 文件/目录存javascript在性检查2. 批量创建目录3. 文件

pip无法安装osgeo失败的问题解决

《pip无法安装osgeo失败的问题解决》本文主要介绍了pip无法安装osgeo失败的问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 进入官方提供的扩展包下载网站寻找版本适配的whl文件注意:要选择cp(python版本)和你py

Linux修改pip和conda缓存路径的几种方法

《Linux修改pip和conda缓存路径的几种方法》在Python生态中,pip和conda是两种常见的软件包管理工具,它们在安装、更新和卸载软件包时都会使用缓存来提高效率,适当地修改它们的缓存路径... 目录一、pip 和 conda 的缓存机制1. pip 的缓存机制默认缓存路径2. conda 的缓