11.28自顶向下 DFS完结
2024/11/28
988. 从叶结点开始的最小字符串
思路:遍历所有路径,每次翻转后加入字符串数组path,结束后对path排序,返回
path[0].按照题目的定义,不能在递归过程中每次比较节点的值取小的路径,因为值大的路径可能反而更短。如样例:
因此需要遍历所有路径最后翻转后比较。
同时注意把值->字符 用
‘a’ + val思路2.不创建字符串数组path了,维护一个全局最小路径,每次拼接字符时接到前面,每次遇到叶节点就比较更新全局最小路径minPath。
class Solution {
vector<string> path;
void dfs(TreeNode* root , string s){
if(!root) return;
s += 'a' + root->val;
if(root->left == root->right){
reverse(s.begin() , s.end());
path.push_back(s);
return;
}
dfs(root->left , s);
dfs(root->right , s);
}
public:
string smallestFromLeaf(TreeNode* root) {
dfs(root , "");
sort(path.begin() , path.end());
return path[0];
}
};
class Solution {
string minPath = "";
void dfs(TreeNode* root , string path){
if(!root) return;
path = char(root->val + 'a') + path;//反向拼接到前面
if(root->left == root->right){
if(minPath.empty() || minPath > path) minPath = path;//遇到了叶子节点则比较全局最小路径与当前路径
}
dfs(root->left , path);
dfs(root->right , path);
}
public:
string smallestFromLeaf(TreeNode* root) {
dfs(root , "");
return minPath;
}
};
1026. 节点与其祖先之间的最大差值 - 力扣(LeetCode)
思路:自顶向下和自底向上
- 自顶向下
题意即找每条路径上的最大差值,取全局最大值。
如果涉及找每条路径,需要传递的参数应该有每条路径上的
最大值mx,最小值mn。遇到路径中每个节点都更新
mx , mn。每次在遇到空节点时才更新答案,因为此时最大差值一定是
mx - mn。
- 自底向上
方法一的思路是维护 B 的祖先节点中的最小值和最大值,我们还可以站在祖先 A 的视角,维护 A 子孙节点中的最小值 mn 和最大值 mx。
换句话说,最小值和最大值不再作为入参,而是作为返回值,意思是以 A 为根的子树中的最小值 mn 和最大值 mx。
递归到节点 A 时,先递归左右子树,拿到左右子树的最小值和最大值。那么:
mn = min
mx = max
然后计算
max(A.val−mn,mx−A.val)更新ans。评价:
第一种自顶向下的递的做法,结合二叉树的
前序遍历,其做法是每次先记录当前结点的所对结果造成的影响,然后把这个影响(路径最大最小值,参数)向下传递,从而有新的影响(如向下传参),最终根据得到的影响可得到题目所求结果。第二种自底向上的归的做法,结合二叉树的
后续遍历,其做法是先往下遍历到底部,从底部开始往上传返回值(return)也就是所说的归,然后每次用从底部归来的返回值记录相应的影响,最终也能求出结果。两者的做法很好的体现了前序遍历和后续遍历的特点。
作为新手一般更为容易直接想到的可能是自顶向下,但是自底向上的做法也应该掌握。
若要学好动态规划,必须掌握自底向上的思考方式。 DP 题目会涉及到原问题和子问题的关系,只有先解决了规模更小子问题,才能解决规模更大的问题。这和二叉树自底向上的思考模式是一致的。
class Solution {
int ans = 0;
void dfs(TreeNode* root ,int mn , int mx){
if(!root){
ans = max(ans , mx - mn);
return;
}
mn = min(mn , root->val);
mx = max(mx , root->val);
dfs(root->left , mn , mx);
dfs(root->right , mn , mx);
}
public:
int maxAncestorDiff(TreeNode* root) {
dfs(root , root->val , root->val);
return ans;
}
};
class Solution {
int ans = 0;
pair<int , int> dfs(TreeNode* node){
if(!node) return{ INT_MAX , INT_MIN};
//左右中的后序遍历方式,先遍历到底部叶子节点,自底向上求左右子树的最小最大值
auto [l_mn , l_mx] = dfs(node->left);
auto [r_mn , r_mx] = dfs(node->right);
int mn = min(node->val , min(l_mn , r_mn));
int mx = max(node->val , max(l_mx ,r_mx));
ans = max(ans , max(node->val - mn , mx - node->val));
return {mn , mx};
}
public:
int maxAncestorDiff(TreeNode* root) {
dfs(root);
return ans;
}
};
1022. 从根到叶的二进制数之和
思路:自顶向下遍历每条路径即可。
class Solution {
int ans = 0;
void dfs(TreeNode* node , int sum){
if(!node) return;
sum = sum * 2 + node->val;
if(node->left == node->right){
ans += sum;
return;
}
dfs(node->left , sum);
dfs(node->right , sum);
}
public:
int sumRootToLeaf(TreeNode* root) {
dfs(root , 0);
return ans;
}
};
623. 在二叉树中增加一行
思路:基本和链表插入节点相同,dfs里参数只需一个cur记录当前深度即可。
BFS的写法注释:中间的判断层数必须放在while(size --)里面,因为该层所有节点都要新建一层,而不仅仅对一个节点新建。如样例:
class Solution {
int d , v;
void dfs(TreeNode* node , int cur){
if(!node) return;
if(cur == d - 1){
TreeNode* l = new TreeNode(v ,node->left , NULL);
TreeNode* r = new TreeNode(v , NULL , node->right);
node->left = l , node->right = r;
}
dfs(node->left , cur + 1);
dfs(node->right , cur + 1);
}
public:
TreeNode* addOneRow(TreeNode* root, int val, int depth) {
d = depth , v = val;
if(d == 1){
return new TreeNode(v , root , NULL);
}
dfs(root , 1);
return root;
}
};
//BFS的写法
class Solution {
public:
TreeNode* addOneRow(TreeNode* root, int val, int depth) {
int d = depth , v = val;
int cur = 1;
if(d == 1){
return new TreeNode(v , root , NULL);
}
queue<TreeNode*> que;
que.push(root);
while(!que.empty()){
int size = que.size();
while(size --){
TreeNode* node = que.front();
if(cur == d - 1){//注意该段代码位置
TreeNode* l = new TreeNode(v , node->left , NULL);
TreeNode* r = new TreeNode(v , NULL , node->right);
node->left = l , node->right = r;
}
que.pop();
if(node->left) que.push(node->left);
if(node->right) que.push(node->right);
}
cur ++;
}
return root;
}
};
1372. 二叉树中的最长交错路径
思路:
这道题要统计最长的交错路径。实际上它只是在传统的统计长度上加了一个限制。
如果没有 交错 这一个限制,我们要找一棵树的最长的路径应该如何去找?dfs参数里加一个len传递下去即可。
加了交错的限制后,我们每次往子节点递归时,只有往与之前的方向相反长度才能在之前的基础上增加,否则就相当于以当前节点为一个新的起点去搜索。
递归的时候除了当前节点
node额外携带两个信息:到达该节点的长度len和到达该节点的方向isLeft;每得到一个新的长度就更新全局最大长度res。
class Solution {
int res = 0;
void dfs(TreeNode* node , int len , bool isLeft){
if(!node) return;
res = max(res , len);
dfs(node->left , isLeft? 1 : len + 1 , true);
dfs(node->right , isLeft? len + 1 : 1 , false);
}
public:
int longestZigZag(TreeNode* root) {
dfs(root , 0 , true);
return res;
}
};
971. 翻转二叉树以匹配先序遍历
思路:
翻转前的先序遍历为
根->左->右,翻转后的先序遍历为根->右->左,所以我们可以对二叉树进行先序遍历,如果当前节点的左子节点的值不等于正确的值,改变深度优先遍历的顺序,先遍历右子树,再遍历左子树。注意 : dfs参数里的 i 索引需要加
&。在C++中,&符号用于引用传递。当你在函数参数中使用&,你实际上是在告诉编译器,你想要传递变量的引用而不是它的值。这意味着函数内部对参数的任何修改都会反映到原始变量上。
int &i是一个引用传递,它允许dfs函数修改i的值,并且这个修改会影响到调用dfs函数时传递的i的原始值。这样做的目的是为了在递归调用dfs函数时跟踪当前遍历到的节点在voyage数组中的位置。这里是
dfs函数中i的作用:
i用于记录当前遍历到的voyage数组中的元素索引。- 在每次递归调用
dfs时,i的值会增加if (root->val != voyage[i++])这里实现,以便于下一个节点的值可以与voyage数组中的下一个元素进行比较。- 由于
i是通过引用传递的,所以每次递归调用中的i实际上是同一个变量。这意味着在递归的任何一层中对i的修改都会影响其他所有层。如果不使用引用传递(即
int i而不是int &i),每次递归调用dfs时都会创建i的一个新副本,这样每一层递归中的i值都是独立的,无法实现跟踪voyage数组中当前位置的目的。
class Solution
{
public:
bool dfs(TreeNode *root, vector<int> &voyage, int &i, vector<int> &res)
{
if (!root)
return true;
if (root->val != voyage[i++])
return false; // 如果当前节点的值不等于当前遍历到的值,返回false
if (root->left && root->left->val != voyage[i])
{
// 如果左子树存在,且左子树的值不等于当前遍历到的值, 说明需要翻转
res.push_back(root->val);
return dfs(root->right, voyage, i, res) && dfs(root->left, voyage, i, res); // 改变深度优先遍历的顺序
}
return dfs(root->left, voyage, i, res) && dfs(root->right, voyage, i, res); // 否则按照正常的顺序遍历
}
vector<int> flipMatchVoyage(TreeNode *root, vector<int> &voyage)
{
vector<int> res;
int i = 0; // 当前遍历到的节点
if (dfs(root, voyage, i, res))
return res;
return {-1};
}
};
路径问题汇总
对于刚刚接触树的问题的新手而言,路径问题是一个比较棘手的问题。题解中关于二叉树路径问题的总结还偏少,今天我用一篇文章总结一下二叉树的路径问题。学透这篇文章,二叉树路径题可以秒杀。
问题分类
二叉树路径的问题大致可以分为两类:
- 自顶向下:
顾名思义,就是从某一个节点(不一定是根节点),从上向下寻找路径,到某一个节点(不一定是叶节点)结束
具体题目如下:而继续细分的话还可以分成一般路径与给定和的路径
- 非自顶向下:就是从任意节点到任意节点的路径,不需要自顶向下
解题模板
这类题通常用深度优先搜索(DFS)和广度优先搜索(BFS)解决,BFS较DFS繁琐,这里为了简洁只展现DFS代码
下面是我对两类题目的分析与模板。
一、自顶而下:dfs
- 一般路径:
vector<vector<int>>res;
void dfs(TreeNode*root,vector<int>path)
{
if(!root) return; //根节点为空直接返回
path.push_back(root->val); //作出选择
if(!root->left && !root->right) //如果到叶节点
{
res.push_back(path);
return;
}
dfs(root->left,path); //继续递归
dfs(root->right,path);
}
- 给定和的路径:
void dfs(TreeNode*root, int sum, vector<int> path)
{
if (!root)
return;
sum -= root->val;
path.push_back(root->val);
if (!root->left && !root->right && sum == 0)
{
res.push_back(path);
return;
}
dfs(root->left, sum, path);
dfs(root->right, sum, path);
}
这类题型DFS注意点:
-
如果是找路径和等于给定
target的路径的那么可以不用新增一个临时变量cursum来判断当前路径和,
只需要用给定和target减去节点值,最终结束条件判断target==0即可 -
是否要回溯:二叉树的问题
大部分是不需要回溯的,原因如下:
二叉树的递归部分:dfs(root->left),dfs(root->right)已经把可能的路径穷尽了,
因此到任意叶节点的路径只可能有一条,绝对不可能出现另外的路径也到这个满足条件的叶节点的;
而对比二维数组(例如迷宫问题)的DFS , for循环向四个方向查找每次只能朝向一个方向,并没有穷尽路径,
因此某一个满足条件的点可能是有多条路径到该点的
并且visited数组标记已经走过的路径是会受到另外路径是否访问的影响,这时候必须回溯
-
找到路径后是否要return:
取决于题目是否要求找到叶节点满足条件的路径,如果必须到叶节点,那么就要return;
但如果是到任意节点都可以,那么必不能return,因为这条路径下面还可能有更深的路径满足条件,还要在此基础上继续递归 -
是否要双重递归(即调用根节点的dfs函数后,继续调用根左右节点的
pathsum函数):看题目要不要求从根节点开始的,还是从任意节点开始
二、非自顶而下:
这类题目一般解题思路如下:
设计一个辅助函数maxpath,调用自身求出以一个节点为根节点的左侧最长路径left和右侧最长路径right,那么经过该节点的最长路径就是left+right
接着只需要从根节点开始dfs,不断比较更新全局变量即可
int res=0;
int maxPath(TreeNode *root) //以root为路径起始点的最长路径
{
if (!root)
return 0;
int left=maxPath(root->left);
int right=maxPath(root->right);
res = max(res, left + right + root->val); //更新全局变量
return max(left, right); //返回左右路径较长者
}
这类题型DFS注意点:
-
left,right代表的含义要根据题目所求设置,比如最长路径、最大路径和等等
-
全局变量res的初值设置是0还是INT_MIN要看题目节点是否存在负值,如果存在就用
INT_MIN,否则就是0 -
注意两点之间路径为1,因此一个点是不能构成路径的
题目分析
下面是对具体题目的分析和代码呈现
一、自顶向下
257. 二叉树的所有路径
直接套用模板1即可,注意把"->"放在递归调用中
vector<string> res;
vector<string> binaryTreePaths(TreeNode<T> *root)
{
dfs(root, "");
return res;
}
void dfs(TreeNode*root, string path)
{
if (!root)
return;
path += to_string(root->val);
if (!root->left && !root->right)
{
res.push_back(path);
return;
}
dfs(root->left, path+"->");
dfs(root->right, path+"->");
}
113. 路径总和 II
直接套用模板2
vector<vector<int>> res;
vector<vector<int>> pathSum(TreeNode *root, int targetSum)
{
vector<int> path;
dfs(root, targetSum, path);
return res;
}
void dfs(TreeNode*root, int sum, vector<int> path)
{
if (!root)
return;
sum -= root->val;
path.push_back(root->val);
if (!root->left && !root->right && sum == 0)
{
res.push_back(path);
return;
}
dfs(root->left, sum, path);
dfs(root->right, sum, path);
}
437. 路径总和 III
双重递归:先调用dfs函数从root开始查找路径,再调用pathsum函数到root左右子树开始查找
套用模板2
int count = 0;
int pathSum(TreeNode *root, int targetSum)
{
if (!root)
return 0;
dfs1(root, targetSum); //以root为起始点查找路径
pathSum(root->left, targetSum); //左子树递归
pathSum(root->right, targetSum); //右子树递归
return count;
}
void dfs(TreeNode *root, int sum)
{
if (!root)
return;
sum -= root->val;
if (sum == 0) //注意不要return,因为不要求到叶节点结束,所以一条路径下面还可能有另一条
count++; //如果找到了一个路径全局变量就+1
dfs1(root->left, sum);
dfs1(root->right, sum);
}
988. 从叶结点开始的最小字符串
换汤不换药,套用模板1
vector<string> path;
string smallestFromLeaf(TreeNode *root)
{
dfs(root, "");
sort(path.begin(), path.end()); //升序排序
return path[0];
}
void dfs(TreeNode *root, string s)
{
if (!root)
return;
s += 'a' + root->val;
if (!root->left && !root->right)
{
reverse(s.begin(), s.end()); //题目要求从根节点到叶节点,因此反转
path.push_back(s);
return;
}
dfs(root->left, s);
dfs(root->right, s);
}
二、非自顶向下
124. 二叉树中的最大路径和
/left,right分别为根节点左右子树最大路径和,注意:如果最大路径和<0,意味着该路径和对总路径和做负贡献,因此不要计入到总路径中,将它设置为0
int res = INT_MIN; //注意节点值可能为负数,因此要设置为最小值
int maxPathSum(TreeNode *root)
{
maxPath(root);
return res;
}
int maxPath(TreeNode *root) //以root为路径起始点的最长路径
{
if (!root)
return 0;
int left = max(maxPath(root->left), 0);
int right = max(maxPath(root->right), 0);
res = max(res, left + right + root->val); //比较当前最大路径和与左右子树最长路径加上根节点值的较大值,更新全局变量
return max(left + root->val, right + root->val); //返回左右子树较长的路径加上根节点值
}
int longestUnivaluePath(TreeNode *root)
{
if (!root)
return 0;
longestPath(root);
return res;
}
int longestPath(TreeNode *root)
{
if (!root)
return 0;
int left = longestPath(root->left), right = longestPath(root->right);
// 如果存在左子节点和根节点同值,更新左最长路径;否则左最长路径为0
if (root->left && root->val == root->left->val)
left++;
else
left = 0;
if (root->right && root->val == root->right->val)
right++;
else
right = 0;
res = max(res, left + right);
return max(left, right);
}
int res1 = 0;
int diameterOfBinaryTree(TreeNode *root)
{
maxPath(root);
return res1;
}
int maxPath(TreeNode *root)
{
// 这里递归结束条件要特别注意:不能是!root(而且不需要判断root为空,因为只有非空才会进入递归),因为单个节点路径长也是0
if (!root->left && !root->right)
return 0;
int left = root->left ? maxPath(root->left) + 1 : 0; //判断左子节点是否为空,从而更新左边最长路径
int right = root->right ? maxPath(root->right) + 1 : 0;
res1 = max(res, left + right); //更新全局变量
return max(left, right); //返回左右路径较大者
}





浙公网安备 33010602011771号