11.28自顶向下 DFS完结

2024/11/28

988. 从叶结点开始的最小字符串

思路:遍历所有路径,每次翻转后加入字符串数组path,结束后对path排序,返回path[0].

按照题目的定义,不能在递归过程中每次比较节点的值取小的路径,因为值大的路径可能反而更短。如样例:

image-20241128162735063

因此需要遍历所有路径最后翻转后比较。

同时注意把值->字符 用‘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)

思路:自顶向下和自底向上

  1. 自顶向下

题意即找每条路径上的最大差值,取全局最大值。

如果涉及找每条路径,需要传递的参数应该有每条路径上的最大值mx最小值mn

遇到路径中每个节点都更新mx , mn

每次在遇到空节点时才更新答案,因为此时最大差值一定是mx - mn

  1. 自底向上

方法一的思路是维护 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 --)里面,因为该层所有节点都要新建一层,而不仅仅对一个节点新建。如样例:

image-20241128183240722

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传递下去即可。

image-20240825232234709.png

加了交错的限制后,我们每次往子节点递归时,只有往与之前的方向相反长度才能在之前的基础上增加,否则就相当于以当前节点为一个新的起点去搜索。

image-20240825232315454.png

递归的时候除了当前节点 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 的作用:

  1. i 用于记录当前遍历到的 voyage 数组中的元素索引。
  2. 在每次递归调用 dfs 时,i 的值会增加 if (root->val != voyage[i++])这里实现,以便于下一个节点的值可以与 voyage 数组中的下一个元素进行比较。
  3. 由于 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};
    }
};

路径问题汇总

对于刚刚接触树的问题的新手而言,路径问题是一个比较棘手的问题。题解中关于二叉树路径问题的总结还偏少,今天我用一篇文章总结一下二叉树的路径问题。学透这篇文章,二叉树路径题可以秒杀。

问题分类

二叉树路径的问题大致可以分为两类:

  1. 自顶向下:
    顾名思义,就是从某一个节点(不一定是根节点),从上向下寻找路径,到某一个节点(不一定是叶节点)结束
    具体题目如下:

而继续细分的话还可以分成一般路径给定和的路径

  1. 非自顶向下:就是从任意节点到任意节点的路径,不需要自顶向下

解题模板

这类题通常用深度优先搜索(DFS)和广度优先搜索(BFS)解决,BFS较DFS繁琐,这里为了简洁只展现DFS代码
下面是我对两类题目的分析与模板。

一、自顶而下:dfs

  1. 一般路径:
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);
}
  1. 给定和的路径:
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注意点:

  1. 如果是找路径和等于给定target的路径的那么可以不用新增一个临时变量cursum来判断当前路径和,
    只需要用给定和target减去节点值,最终结束条件判断target==0即可

  2. 是否要回溯:二叉树的问题大部分是不需要回溯的,原因如下:
    二叉树的递归部分:dfs(root->left),dfs(root->right)已经把可能的路径穷尽了,
    因此到任意叶节点的路径只可能有一条,绝对不可能出现另外的路径也到这个满足条件的叶节点的;

​ 而对比二维数组(例如迷宫问题)的DFS , for循环向四个方向查找每次只能朝向一个方向,并没有穷尽路径,
​ 因此某一个满足条件的点可能是有多条路径到该点的

​ 并且visited数组标记已经走过的路径是会受到另外路径是否访问的影响,这时候必须回溯

  1. 找到路径后是否要return:
    取决于题目是否要求找到叶节点满足条件的路径,如果必须到叶节点,那么就要return;
    但如果是到任意节点都可以,那么必不能return,因为这条路径下面还可能有更深的路径满足条件,还要在此基础上继续递归

  2. 是否要双重递归(即调用根节点的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注意点:

  1. left,right代表的含义要根据题目所求设置,比如最长路径、最大路径和等等

  2. 全局变量res的初值设置是0还是INT_MIN要看题目节点是否存在负值,如果存在就用INT_MIN,否则就是0

  3. 注意两点之间路径为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); //返回左右子树较长的路径加上根节点值
}

687. 最长同值路径

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);
}

543. 二叉树的直径

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);  //返回左右路径较大者
}

链接:https://leetcode.cn/problems/smallest-string-starting-from-leaf/solutions/815689/yi-pian-wen-zhang-jie-jue-suo-you-er-cha-10sk/

posted @ 2024-11-28 20:48  七龙猪  阅读(2)  评论(0)    收藏  举报
-->