05、二叉树的最大路径和

厨子大约 6 分钟数据结构算法算法基地面试深搜广搜二叉树刷题

哈喽,大家好,我是厨子,今天给大家分享一个经典题目

https://leetcode.cn/problems/binary-tree-maximum-path-sum/open in new window

题目描述

二叉树中的路径被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。

同一个节点在一条路径序列中至多出现一次。该路径至少包含一个节点,且不一定经过根节点。

路径和是路径中各节点值的总和。

给你一个二叉树的根节点 root,返回其最大路径和。

示例 1:

输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6
    1
   / \
  2   3

示例 2:

输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42
      -10
      /  \
     9   20
        /  \
       15   7

约束条件:

  • 树中节点数目范围是 [1, 3 * 10^4]
  • -1000 <= Node.val <= 1000

什么是路径?

路径的定义

路径特点:
1. 至少包含一个节点
2. 节点至多出现一次
3. 相邻节点之间有边连接
4. 不一定经过根节点 ⭐

合法路径示例:
      5
     / \
    4   8
   /   / \
  11  13  4
 /  \      \
7    2      1

路径1: [5] (单个节点) ✓
路径2: [4,5,8] (经过根节点) ✓
路径3: [7,11,4] (不经过根节点) ✓
路径4: [11,4,5,8,13] (经过根节点) ✓

非法路径:
[11,4,11] ✗ (11 出现两次)
[7,11,13] ✗ (11 到 13 没有直接边)

通过以上例子大家应该知道什么是路径了,那咱们下面对路径进行分析

问题分析

核心难点

难点1: 路径不一定经过根节点
解决: 需要遍历每个节点,尝试以它为拐点

难点2: 路径可以拐弯
解决: 对于每个节点,考虑 左子树+节点+右子树

难点3: 节点值可能为负
解决: 如果子树路径和为负,不如不选

难点4: 返回给父节点的值 ≠ 经过当前节点的最大路径
解决: 区分两个概念
  - 经过当前节点的最大路径和(可以拐弯)
  - 从当前节点出发的最大路径和(不能拐弯)

两个重要概念

概念1: 以 node 为拐点的最大路径和
= node.val + max(0, 左子树贡献) + max(0, 右子树贡献)
说明: 可以同时选择左右子树(形成拐弯路径)

概念2: 从 node 出发向上的最大贡献值
= node.val + max(0, max(左子树贡献, 右子树贡献))
说明: 只能选择左或右其中之一(不能拐弯)

示例:
      -10
      /  \
     9   20
        /  \
       15   7

节点 20:
- 作为拐点的路径: 15 + 20 + 7 = 42 ✓
- 向上的贡献: 20 + max(15, 7) = 35
  (因为不能同时选15和7,否则父节点-10会形成分叉)

解题思路

递归(后序遍历)⭐⭐⭐

核心思想

递归思路:
1. 对于每个节点,计算以它为拐点的最大路径和
2. 更新全局最大值
3. 返回从该节点出发向上的最大贡献值

递归返回值:
从当前节点出发,向父节点延伸的最大路径和
= node.val + max(0, max(leftGain, rightGain))

全局变量:
maxSum: 记录所有节点作为拐点时的最大路径和

图解分析

root = [-10,9,20,null,null,15,7] 为例:

树结构:
      -10
      /  \
     9   20
        /  \
       15   7

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

后序遍历过程:

1. 访问叶子节点 15:
   leftGain = 0 (没有左子树)
   rightGain = 0 (没有右子树)

   以 15 为拐点的路径和:
   = 15 + max(0, 0) + max(0, 0) = 15
   更新 maxSum = 15

   返回给父节点 20 的贡献:
   = 15 + max(0, max(0, 0)) = 15(可以理解为层层向上进贡,每个节点,分别可以贡献多少贡献点)

2. 访问叶子节点 7:
   以 7 为拐点的路径和 = 7
   更新 maxSum = 15 (不变)
   返回贡献 = 7

3. 访问节点 20:
   leftGain = 15 (来自节点15)
   rightGain = 7 (来自节点7)

   以 20 为拐点的路径和:
   = 20 + max(0, 15) + max(0, 7)
   = 20 + 15 + 7 = 42 ⭐
   更新 maxSum = 42(因为有两种情况嘛,一种是以该 node 为拐点,另外一种是给父结点贡献)

   返回给父节点 -10 的贡献:
   = 20 + max(0, max(15, 7))
   = 20 + 15 = 35
   (只能选左或右,不能同时选)

4. 访问叶子节点 9:
   以 9 为拐点的路径和 = 9
   返回贡献 = 9

5. 访问根节点 -10:
   leftGain = 9 (来自节点9)
   rightGain = 35 (来自节点20)

   以 -10 为拐点的路径和:
   = -10 + max(0, 9) + max(0, 35)
   = -10 + 9 + 35 = 34
   maxSum = 42 (不变)

   返回贡献:
   = -10 + max(0, max(9, 35))
   = -10 + 35 = 25

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

最终答案: maxSum = 42
最优路径: 15 → 20 → 7

这时候又有同学要问了,为什么要用 max(0,gain)?那我们来深入剖析一下

原因: 节点值可能为负

示例:
      5
     / \
   -3   2

节点 5:
leftGain = -3
rightGain = 2

如果不用 max(0, gain):
以 5 为拐点 = 5 + (-3) + 2 = 4

用 max(0, gain):
以 5 为拐点 = 5 + max(0, -3) + max(0, 2)
           = 5 + 0 + 2 = 7

结论: 如果子树贡献为负,不如不选

代码实现

核心版本

/**
 * 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 {
private:
    int maxSum;

public:
    int maxPathSum(TreeNode* root) {
        maxSum = INT_MIN;
        maxGain(root);
        return maxSum;
    }

private:
    /**
     * 计算从节点出发的最大路径贡献值
     * 同时更新全局最大路径和
     */
    int maxGain(TreeNode* node) {
        if (node == nullptr) {
            return 0;
        }

        // 递归计算左右子树的最大贡献值
        // 如果贡献值为负,则不选(用0代替)
        int leftGain = max(0, maxGain(node->left));
        int rightGain = max(0, maxGain(node->right));

        // 以当前节点为拐点的路径和
        int pathSum = node->val + leftGain + rightGain;

        // 更新全局最大值
        maxSum = max(maxSum, pathSum);

        // 返回从当前节点出发的最大贡献值
        // 只能选择左或右子树其中之一
        return node->val + max(leftGain, rightGain);
    }
};

时间复杂度: O(n) - 每个节点访问一次

空间复杂度: O(h) - 递归栈深度,h 为树的高度

关键点详解

关键点1: 为什么是后序遍历?

后序遍历顺序: 左 → 右 → 根

原因:
需要先知道左右子树的信息,才能计算当前节点

递归顺序:
1. 先递归左子树,得到 leftGain
2. 再递归右子树,得到 rightGain
3. 最后处理当前节点

如果用前序:
先处理根节点,但不知道左右子树的信息
无法计算以根为拐点的路径和

关键点2: 返回值 vs 更新值

两个不同的值:

值1: 以当前节点为拐点的路径和
= node->val + leftGain + rightGain
用途: 更新全局最大值 maxSum
特点: 可以同时包含左右子树

值2: 从当前节点向上的贡献值
= node->val + max(leftGain, rightGain)
用途: 返回给父节点
特点: 只能包含左或右子树

示例:
      5
     / \
    4   8

节点 5:
leftGain = 4, rightGain = 8

拐点路径和 = 5 + 4 + 8 = 17 (更新 maxSum)
向上贡献 = 5 + max(4, 8) = 13 (返回给父节点)
这就是刚才提到的两种不同的值