专题十_穷举vs暴搜vs深搜vs回溯vs剪枝_二叉树的深度优先搜索_算法专题详细总结
目录
搜索 vs 深度优先遍历 vs 深度优先搜索 vs 宽度优先遍历 vs 宽度优先搜索 vs 暴搜
搜索 vs 深度优先遍历 vs 深度优先搜索 vs 宽度优先遍历 vs 宽度优先搜索 vs 暴搜
1.深度优先遍历 vs 深度优先搜索(dfs)
2.宽度优先遍历 vs 宽度优先搜索(bfs)
那么遍历只是形式,目的是搜索
2.关系图
暴力枚举一遍所有的情况
搜索(暴搜) dfs
3.拓展搜索问题
全排列
决策树
废话不多说,直接进入例题:
1. 计算布尔⼆叉树的值(medium)
解析:
递归函数的调用:
主要就是分三步,当主问题与子问题相同的时候:
1.函数头
2.函数体
3.函数出口
/**
* 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:
bool a[2]={false,true};
bool evaluateTree(TreeNode* root) {
if(root->left==nullptr||root->right==nullptr) return a[root->val];
bool left=evaluateTree(root->left);
bool right=evaluateTree(root->right);
return root->val==2?left|right:left&right;
}
};
总结:
只要能分析出当前问题的相同子函数,就可以得出函数体里面的内容,通过所有相同的子函数的内容进行一步步操作就能完成当前函数的任务。
2. 求根节点到叶节点数字之和(medium)
题意还是比较好理解的,就是对从根节点开始一直到叶子节点,计算每个节点当前值不断*10最后所有叶子节点的和
解析:
这种题目计算每个叶子节点的方法一眼就是dfs,先递归到叶子节点,然后就开始相加。
以下是同一种dfs的两种思想:
1.定义全局变量ret,然后每次递归到叶子节点的时候就开始进行相加,那么我就是用一个字符串s来保存每一层的临时变量,这样可以达到回溯的效果,起始不要字符串保存,只有用presum来存储临时变量也是ok的,都能存的下最终的和,然后每次到叶子节点ret就进行相加,知道加完所有叶子节点的值就完成,返回ret
唯一需要注意的就是,在传入dfs的时候就要立刻加上当前节点的值,这样就不会漏掉当前节点的某一位,因为只要到叶子节点就要开始返回上一层了,为了防止叶子节点的值给漏掉。
/**
* 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:
int ret=0;
int sumNumbers(TreeNode* root) {
string s;
dfs(root,s);
return ret;
}
void dfs(TreeNode* root,string s)
{
s+=to_string(root->val);
if(root->left==nullptr&&root->right==nullptr)
{
ret+=stoi(s);
return;
}
if(root->left) dfs(root->left,s);
if(root->right) dfs(root->right,s);
}
};
2.就是用presum来记录临时变量,不需要字符串到整形,整形到字符串的各种转换,因为最后的结果int也能存下。
看到上面的图,就能看出,如果我存临时变量presum,那么dfs返回的就是当前节点下所有叶子节点的和,那么整个二叉树就都变成了一个子问题,就是要求头节点下所有叶子节点的和。
函数头:int dfs(root,presum);
出口:就是当到达叶子节点的时候,那么就返回当前叶子节点的presum
函数体:用ret来记录当前节点下所有左右子树中,叶子节点的和,在当前节点左子树不为空下,传入dfs(root->left,presum);在当前节点右子树不为空下,传入dfs(root->right,presum);来求出当前节点下的左右子树的所有叶子节点的和
最后返回ret
/**
* 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:
int sumNumbers(TreeNode* root) {
return dfs(root,0);
}
int dfs(TreeNode* root,int presum)
{
presum=presum*10+root->val;
if(root->left==nullptr&&root->right==nullptr) return presum;
int ret=0;//记录当前节点下,所有叶子节点的值
if(root->left) ret+=dfs(root->left,presum);
if(root->right) ret+=dfs(root->right,presum);
return ret; //返回给上一层
}
};
总结:
像这种带有回溯类的问题,只需要考虑清楚带上的临时变量不会再中途被改变,并且再返回上一层稳定即可。
3. ⼆叉树剪枝(medium)
解析:
因为计算机不可能知道当前节点下左右子树的所有节点是否全为0,所以就要完全遍历到叶子节点在开始判断,判断叶子节点是否为0 如果是就直接置为空,并且返回空,否则就返回当前节点,不改变。
经过上面的思考,发现要先解决完所有的左子树和右子树,然后在考虑当前的节点是否为0,且左右子树是否为空,所以就完全是一个后序遍历。
1.函数头:再前面的分析里面要返回当前节点的情况,是否为空,否则就返回当前节点。说明函数头是带有返回值的。
Node* dfs(root);
2.函数体:
由于是后序遍历,先访问所有左子树,再访问右子树。然后进行判断当前节点
root->left=dfs(root->left);
root->right=dfs(root->right);
判断:如果当前节点的左右子树都为空,并且当前节点值为0,就说明这个节点也可以被删掉
if(root->left==nullptr&&root->right==nullptr&&root->val==0) root=nullptr;
return root;//将当前节点的值,返回给上一层
3.递归出口:当前节点为空的时候就返回空
if(root==nullptr) return nulltpr;
/**
* 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:
TreeNode* pruneTree(TreeNode* root) {
if(root==nullptr) return nullptr;
root->left=pruneTree(root->left);
root->right=pruneTree(root->right);
if(root->left==nullptr&&root->right==nullptr&&root->val==0) root=nullptr;
return root;//返回给上一层
}
};
总结:
写的代码很简单,但是要想清楚每一步还是很不容易,多注意细节问题,多思考,一个题目要反复思考才能被吸收。
4. 验证⼆叉搜索树(medium)
题意很简单,只要证明该树是不是二叉搜索树
解析:
策略一:中序遍历,判断这个二叉树是否有序
用prev来记录二叉树的中序的根节点,就是当前节点左子树的最右边的一个节点
要保证该二叉树是平衡二叉树,就要一直保证prev < root->val 那么就能满足条件,一旦打破了就返回false
那么:
1.函数头:bool dfs(root)
2.函数体:
弄弄左子树:bool left=dfs(root->left);
中序判断:bool ret = false;
if(prev<root->val) ret=true;
prev = root->val;弄弄右子树:bool right=dfs(root->right);
3.出口条件:
当节点访问到空的时候,证明没有问题,返回true
/**
* 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:
long prev=LONG_MIN;
bool isValidBST(TreeNode* root) {
if(root==nullptr) return true;
bool left=isValidBST(root->left);
bool ret=false;
if(root->val>prev) ret=true;
prev=root->val;
bool right=isValidBST(root->right);
return left&&right&&ret;
}
};
策略二:剪枝
上面策略一是访问了所有的节点,但是如果只要满足有一个节点不满足平衡二叉树的条件,不满足升序,那么就要返回false,不在往后遍历。
那么只需要再判断左子树的时候证明他是false 或者 再判断当前值不满足升序的时候都返回false就能完成剪枝操作,不在往后执行
/**
* 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:
long prev=LONG_MIN;
bool isValidBST(TreeNode* root) {
if(root==nullptr) return true;
bool left=isValidBST(root->left);
if(left==false) return false;
bool ret=false;
if(root->val>prev) ret=true;
prev=root->val;
if(ret==false) return false;
bool right=isValidBST(root->right);
return left&&right&&ret;
}
};
总结:
回溯、剪枝真的没有那么神秘,再递归里面百分之99都要用到回溯,剪枝只是再我们能完成条件之后再考虑优化,我们可以尝试以下剪枝。
5. ⼆叉搜索树中第 k ⼩的元素(medium)
解析:
设置两个全局变量,ret,count,那么当count==k的时候,说明dfs再中序遍历里面已经遇到了k次中序遍历的值,得到了第k小的值
1.函数头:
void dfs(root);
2.函数体:
中序遍历,遍历到第k次时找到第k小的值:
dfs(root->left);
++count; if(count==k) ret=root->val;
dfs(root->right);
3.出口条件:
知道root==nullptr || ret != -1 就return;只要ret被改变就可以进行剪枝
/**
* 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:
int ret=-1;
int _k;
int count=0;
int kthSmallest(TreeNode* root, int k) {
_k=k;
dfs(root);
return ret;
}
void dfs(TreeNode* root)
{
if(root==nullptr||ret!=-1) return;
dfs(root->left);
if(++count==_k) ret=root->val;
dfs(root->right);
}
};
总结:
这题真的很简单,通过上面的练习,就只需要一次中序遍历就可以得到第k小的值。
6. ⼆叉树的所有路径(easy)
解析:
虽然这题很简单,但只是有这个简单题来突破其他的难题,我们需要考虑到很多细节问题:
回溯 -> 恢复现场 的因果关系,是因为有了回溯才出现的恢复现场,不能本末倒置了。
只要有递归,那么就绝对伴随之回溯,只要有深度优先遍历,就会存在恢复现场。
为了回溯恢复现场的时候,那么就要考虑我们的参数就不能设置为全局变量,只需要把他设置为dfs的参数,那么就不需要对全局变量进行修改或删除。再每一层的临时变量都是存储当前位置的参数,不会被后面递归影响。
再考虑路径值的时候,就只需要考虑前序遍历然后添加到字符串内即可。
/**
* 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:
vector<string> ret;
vector<string> binaryTreePaths(TreeNode* root) {
string s;
dfs(root,s);
return ret;
}
void dfs(TreeNode* root,string s)
{
if(root==nullptr) return;
s+=to_string(root->val);
if(root->left==nullptr&&root->right==nullptr) ret.push_back(s);
else s+="->";
dfs(root->left,s);
dfs(root->right,s);
}
};
总结:
这里对于回溯和恢复现场真的有很强的表现,强烈建议深刻理解以下恢复现场时,把参数当作临时变量真的很方便,就不需要自己再进行操作。
穷举vs暴搜vs深搜vs回溯vs剪枝
什么是回溯算法
回溯算法是⼀种经典的递归算法,通常⽤于解决组合问题、排列问题和搜索问题等。回溯算法的基本思想:从⼀个初始状态开始,按照⼀定的规则向前搜索,当搜索到某个状态⽆法前进时,回退到前⼀个状态,再按照其他的规则搜索。回溯算法在搜索过程中维护⼀个状态树,通过遍历状态树来实现对所有可能解的搜索。回溯算法的核⼼思想:“试错”,即在搜索过程中不断地做出选择,如果选择正确,则继续向前搜索;否则,回退到上⼀个状态,重新做出选择。回溯算法通常⽤于解决具有多个解,且每个解都需要搜索才能找到的问题。
void backtrack(vector<int>& path, vector<int>& choice, ...) {
// 满⾜结束条件
if (/* 满⾜结束条件 */) {
// 将路径添加到结果集中
res.push_back(path);
return;
}
// 遍历所有选择
for (int i = 0; i < choices.size(); i++) {
// 做出选择
path.push_back(choices[i]);
// 做出当前选择后继续搜索
backtrack(path, choices);
// 撤销选择
path.pop_back();
}
}
其中, path 表⽰当前已经做出的选择, choices 表⽰当前可以做的选择。在回溯算法中,我们需要做出选择,然后递归地调⽤回溯函数。如果满⾜结束条件,则将当前路径添加到结果集中;否则,我们需要撤销选择,回到上⼀个状态,然后继续搜索其他的选择。回溯算法的时间复杂度通常较⾼,因为它需要遍历所有可能的解。但是,回溯算法的空间复杂度较低,因为它只需要维护⼀个状态树。在实际应⽤中,回溯算法通常需要通过剪枝等⽅法进⾏优化,以减少搜索的次数,从⽽提⾼算法的效率。
7. 全排列(medium)
解析:
1)暴力枚举:
两层三层循环还可以枚举,如果要排列100层的数,那简直不可能,只能借助递归来思考。
2)递归优化:
1.画出决策树:越详细越好
2.设计代码:
全局变量
int [][] ret
int [] path
bool [] check 实现剪枝
dfs函数
就需要关系某一个节点在干什么事情
细节问题
回溯:
1.干掉path最后一个元素
2.修改check数组
剪枝
递归出口:遇到叶子节点就直接添加结果
class Solution {
public:
vector<vector<int>> ret;
vector<int> path;
bool check[7];
vector<vector<int>> permute(vector<int>& nums) {
dfs(nums);
return ret;
}
void dfs(vector<int>& nums)
{
if(path.size()==nums.size())
{
ret.push_back(path);
return;
}
for(int i=0;i<nums.size();i++)
{
if(check[i]==false)
{
path.push_back(nums[i]);
check[i]=true;
dfs(nums);
path.pop_back();
check[i]=false;
}
}
}
};
总结:
最重要的就是画出决策树,越细致越好,然后通过决策树来观察代码该如何书写。
8. ⼦集(medium)
解析:
解法一:
1.决策树
2.设计代码
全局变量
vector<vector<int>> ret; //来记录整体传入的子集
vector<int> path; //设置全局变量,来保证传参,然后进行手动删除
dfs
细节问题:剪枝,回溯,递归出口
传入nums后,要考虑当前位置是否选择,分为选择和不选择两种
如果选择,那么就是ret.push_back(path+nums[i])
不选择就是ret.push_back(path)
但是这里要注意的就是不选择要在选择前面,这样方便后面选择后的手动删除。再就是dfs传入后就是传入到下一层,i+1,表示的就是下一层的层数,此时回溯到这一层i也不会被改变。
递归出口:就是在i的层数在nums.size()的时候就进行最后的添加,然后进行返回。
、
class Solution {
public:
vector<vector<int>> ret;
vector<int> path;
int check[10];
vector<vector<int>> subsets(vector<int>& nums) {
dfs(nums,0);
return ret;
}
void dfs(vector<int>& nums,int i)
{
if(nums.size()==i)
{
ret.push_back(path);
return;
}
//不选
dfs(nums,i+1);
path.push_back(nums[i]);
//选
dfs(nums,i+1);
path.pop_back();
}
};
解法二:
1.画决策树
2.设计代码
全局变量
vector<vector<int>> ret; //来记录整体传入的子集
vector<int> path; //设置全局变量,来保证传参,然后进行手动删除
dfs
细节问题:回溯 剪枝 递归出口
只用关心每层进去后当前nums下标值的后面的值,前面的值不用考虑
例如:在1中选2,1中选3不选2,就是在for循环内进行的,当进入下一层选2,回来这个数组就会删掉2,i++,再次进入下一层选3.
class Solution {
public:
vector<vector<int>> ret;
vector<int> path;
vector<vector<int>> subsets(vector<int>& nums) {
dfs(nums,0);
return ret;
}
void dfs(vector<int>& nums,int k)
{
ret.push_back(path);
for(int i=k;i<nums.size();i++)
{
path.push_back(nums[i]);
dfs(nums,i+1);
path.pop_back();
}
}
};
总结:
我觉得最关键的就是画出决策图然后再进行思考递归出口或者函数体里面的内容,像这一题就是跟上一题决策树完全不同,思考的方向就完全不同。
总结一下吧~这章对我的进步真的很大,希望对你也是!!!
原文地址:https://blog.csdn.net/2301_80636070/article/details/142699850
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!