力扣hot100之124:二叉树中的最大路径和
力扣hot100之124:二叉树中的最大路径和

LeetCode 124:二叉树中的最大路径和(Binary Tree Maximum Path Sum)题解

Binary Tree Maximum Path Sum 是 LeetCode Hot 100 里一道非常经典、也非常有代表性的二叉树递归题。

这道题和普通的二叉树遍历题不太一样。 前面的很多题,可能只是让你:

  • 求深度

  • 判断是否合法

  • 做前中后序遍历

  • 统计节点数量

但这道题要求的是:

在整棵二叉树中,找到一条路径,使这条路径上的节点值之和最大。

这就意味着,我们不能只盯着“从根到叶子”的路径,也不能只盯着某一个方向。 因为题目中的路径可以:

  • 从某个节点出发

  • 向上经过父节点

  • 再走到另一侧子树

也就是说,这条路径完全可能是:

左子树 -> 当前节点 -> 右子树

而这,正是这道题最核心、也最容易让人绕晕的地方。

树形递归 + 维护单边贡献值 + 全局更新答案

本文就系统讲清楚这道题到底在求什么、为什么递归返回值要这么设计,以及 ans 为什么要单独作为全局变量来维护。


题目介绍

给定一棵二叉树的根节点 root,请你返回这棵树中的最大路径和。

这里的“路径”有几个非常重要的特点:

  • 路径中的节点必须是连通的

  • 每个节点在路径中最多出现一次

  • 路径不一定经过根节点

  • 路径至少包含一个节点

也就是说,路径不要求从根开始,也不要求到叶子结束。 只要是一条沿着父子关系连起来的连续路径,就算合法。

例如下面这棵树:

    1
   / \
  2   3

最大路径和是:

2 + 1 + 3 = 6

因为最优路径就是:

2 -> 1 -> 3

示例

示例 1

输入:
root = [1,2,3]

输出:
6

对应二叉树:

    1
   / \
  2   3

最大路径是:

2 -> 1 -> 3

路径和为 6

示例 2

输入:
root = [-10,9,20,null,null,15,7]

输出:
42

对应二叉树:

      -10
      /  \
     9   20
        /  \
       15   7

最大路径是:

15 -> 20 -> 7

路径和为:

15 + 20 + 7 = 42

注意这里的最优路径并没有经过根节点 -10 这也是这道题最容易忽略的一点。


这道题难在哪里?

很多人第一次看到这题时,最容易下意识想到的是:

能不能求每个节点到叶子的最大路径和?

但很快会发现不对,因为这题要求的不是“单边路径”,而是整棵树中的任意路径。

比如某条最大路径可能长这样:

左边一条链 -> 当前节点 -> 右边一条链

这意味着:

  • 对于某个节点来说,它有可能同时使用左子树和右子树

  • 但如果这个值要返回给父节点时,却不能左右两边都带上

因为路径一旦返回给父节点,还要继续往上延伸。 而一条合法路径不能在某个节点处分叉成三条路。

所以这道题真正的难点就在于:

“当前节点作为路径最高点时的答案” “当前节点向父节点提供的最大贡献值”

这两个东西不是同一个概念。

只要把这两个概念分清楚,这题就通了。


核心思路

对于每一个节点,我们实际上要思考两件事:

第一件事:如果把当前节点当作路径的“拐点”或“最高点”,最大路径和是多少?

也就是:

左边贡献 + 当前节点值 + 右边贡献

这条路径在当前节点这里“收口”,不会再继续往上延伸。

它的作用是:

用来更新整棵树的最终答案。


第二件事:如果当前节点要把路径继续往上交给父节点,它最多能提供多少贡献?

因为父节点如果还要接这条路径,只能从当前节点这里选一边继续往上走:

  • 要么走左边

  • 要么走右边

  • 不可能左右都带上

所以返回给父节点的值只能是:

当前节点值 + max(左贡献, 右贡献)

但如果某一边贡献是负数,那么带上它反而会让路径变差。 所以还要和 0 比较,负贡献直接舍弃。

于是递归返回值就变成了:

max(当前节点值 + max(左贡献, 右贡献), 0)

这就是你代码里最后那句返回语句的含义。


Java 代码

class Solution {
    private int ans = Integer.MIN_VALUE;

    public int maxPathSum(TreeNode root) {
        dfs(root);
        return ans;
    }

    private int dfs(TreeNode node) {
        if (node == null) {
            return 0; 
        }

        int lVal = dfs(node.left); 
        int rVal = dfs(node.right); 

        ans = Math.max(ans, lVal + rVal + node.val); 

        return Math.max(Math.max(lVal, rVal) + node.val, 0); 
    }
}

这段代码不长,但信息量非常大。 其中最关键的地方有两个:

  • ans = Math.max(ans, lVal + rVal + node.val);

  • return Math.max(Math.max(lVal, rVal) + node.val, 0);

一个负责更新全局最大路径和。 一个负责返回当前节点对父节点的最大贡献。


代码拆解

下面一部分一部分来看。


1. 为什么要有一个全局变量 ans

private int ans = Integer.MIN_VALUE;

这个变量表示:

整棵树当前找到的最大路径和。

为什么它必须是全局变量?

因为我们在递归过程中,遍历到每一个节点时,都有可能发现一条更优路径。 而这条路径不一定会被递归返回给上层。

比如:

15 -> 20 -> 7

这条路径会在节点 20 这里被计算出来,但它不会作为一个完整路径继续返回给 -10 因为返回给父节点时只能返回单边贡献,而不能把左右两边一起带上。

所以:

  • “完整路径和”只能用来更新全局答案

  • “单边最大贡献”才是递归返回值

这就是 ans 存在的原因。

另外,初始值设成 Integer.MIN_VALUE 也很重要。 因为树中的节点值可能全是负数,如果初始值设成 0,就会出错。


2. maxPathSum 函数在做什么?

public int maxPathSum(TreeNode root) {
    dfs(root);
    return ans;
}

这个函数非常简单:

  1. 通过 dfs(root) 遍历整棵树

  2. 在遍历过程中不断更新 ans

  3. 最后返回全局最优结果

也就是说,真正的逻辑都在 dfs 里面。


3. 为什么空节点返回 0

if (node == null) {
    return 0; 
}

这表示:

空节点对路径没有贡献。

因为路径和本质上是节点值相加,空节点不存在,自然贡献为 0

这样写还有一个好处,就是递归逻辑会非常统一:

  • 左子树不存在,就返回 0

  • 右子树不存在,就返回 0

后面直接拿来计算即可。


4. lValrVal 分别表示什么?

int lVal = dfs(node.left); 
int rVal = dfs(node.right); 

这里的 lValrVal,并不是“左子树的最大路径和”“右子树的最大路径和”。

它们表示的是:

左孩子 / 右孩子能够向当前节点提供的最大单边贡献值。

注意这个定义很重要。 它不是某棵子树内部的完整答案,而是“如果我要经过这个孩子继续往下走,最多能拿到多少收益”。

比如某个节点左边有一条很优的路径,但它如果已经在左子树内部形成了一个完整拐点路径,那部分不会整个返回上来。 返回上来的只能是一条单链。


5. 为什么更新答案用 lVal + rVal + node.val

ans = Math.max(ans, lVal + rVal + node.val); 

因为这代表的是:

以当前节点为最高点时,能形成的最大路径和。

路径长这样:

左边一条贡献链 -> 当前节点 -> 右边一条贡献链

所以和就是:

左贡献 + 当前节点值 + 右贡献

这一步非常关键,因为整棵树中的最大路径,很可能就是在某个节点这里“左 + 中 + 右”拼出来的。

例如:

    1
   / \
  2   3

在节点 1 这里:

  • 左贡献是 2

  • 右贡献是 3

  • 当前节点值是 1

所以路径和就是:

2 + 1 + 3 = 6

这正是答案。


6. 为什么返回值只能选一边?

return Math.max(Math.max(lVal, rVal) + node.val, 0); 

这是整道题最核心的地方。

当前节点如果要把贡献继续传给父节点,那么路径只能沿着一条分支往上走。 因为父节点如果再接上来,就已经形成更长的一条链了。

这时候如果你把左右两边都返回给父节点,相当于路径在当前节点发生了分叉。 那就不是一条合法路径了。

所以返回值只能是:

当前节点值 + max(左贡献, 右贡献)

表示“从当前节点出发,向下一路延伸,最多能提供多少收益”。

但是,如果这条贡献值是负数呢?

比如:

  • 左子树很差

  • 带上它反而让路径和变小

那还不如不要。 所以最后还要和 0 比较:

max(当前节点值 + max(左贡献, 右贡献), 0)

这表示:

  • 如果带上某一边有收益,就带上

  • 如果没有收益,就干脆从当前节点这里截断


手动模拟一遍

我们用经典例子来完整走一遍:

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

二叉树结构是:

      -10
      /  \
     9   20
        /  \
       15   7

第一步:处理节点 15

节点 15 左右都是空节点。

所以:

lVal = 0
rVal = 0

更新答案:

ans = max(-∞, 0 + 0 + 15) = 15

返回给父节点的贡献值:

max(max(0, 0) + 15, 0) = 15

第二步:处理节点 7

同理:

lVal = 0
rVal = 0

更新答案:

ans = max(15, 0 + 0 + 7) = 15

返回贡献值:

7

第三步:处理节点 20

此时:

  • 左贡献 lVal = 15

  • 右贡献 rVal = 7

更新答案:

ans = max(15, 15 + 7 + 20) = 42

这说明以 20 为最高点时,形成了一条很优路径:

15 -> 20 -> 7

返回给父节点的贡献值只能选一边:

max(max(15, 7) + 20, 0) = 35

也就是说,节点 20 最多向上提供 35 的收益,对应路径:

20 -> 15

或者写成从下往上看就是:

15 -> 20

第四步:处理节点 9

同理:

lVal = 0
rVal = 0

更新答案:

ans = max(42, 9) = 42

返回贡献值:

9

第五步:处理根节点 -10

现在:

  • 左贡献 lVal = 9

  • 右贡献 rVal = 35

更新答案:

ans = max(42, 9 + 35 - 10) = 42

也就是说,经过根节点的路径和是:

9 + (-10) + 35 = 34

它并没有超过之前在节点 20 处得到的 42

所以最终答案仍然是:

42

为什么全是负数时也成立?

比如这棵树:

   -3

递归到节点 -3 时:

  • 左贡献 = 0

  • 右贡献 = 0

更新答案:

ans = max(-∞, -3) = -3

返回值:

max(-3, 0) = 0

虽然返回给父节点的是 0,但全局答案已经被正确更新成 -3 了。

这也是为什么:

  • 返回值可以和 0 比较

  • ans 必须初始化成负无穷

否则全负数情况就会出错。


这道题真正想考什么?

这道题表面上是在求最大路径和,但本质上考察的是:

树形递归中,如何区分“子问题返回值”和“全局答案”。


第一,是否理解递归返回值的含义

很多树题最难的地方,不是递归本身,而是:

递归函数到底应该返回什么?

这题中,dfs(node) 返回的不是“以 node 为根的最大路径和”,而是:

node 作为一条单边路径起点时,能向父节点提供的最大贡献值。

这个定义一旦想清楚,代码就顺了。


第二,是否理解“经过当前节点的完整答案”和“向上延伸的局部贡献”是不同的

这题最容易混淆的就是这两者。

  • lVal + rVal + node.val:是当前节点作为拐点时的完整路径和

  • max(lVal, rVal) + node.val:是当前节点能向上返回的单边贡献

前者拿来更新答案。 后者拿来参与递归。

它们不能混为一谈。


第三,是否会处理负贡献

如果某棵子树返回的是负值,说明带上它只会拖后腿。 那就应该直接舍弃。

所以返回值和 0 比较,是这道题非常关键的一步。

这其实也是很多“最大路径”“最大收益”问题里常见的处理方式:

负贡献直接截断。


复杂度分析

假设二叉树一共有 n 个节点。

时间复杂度

每个节点都会被访问一次,每次只进行常数级计算,所以时间复杂度是:

O(n)

空间复杂度

空间主要来自递归调用栈。

  • 如果树比较平衡,递归深度大约是 O(log n)

  • 如果树退化成链表,递归深度最坏是 O(n)

所以空间复杂度可以写成:

O(h)

其中 h 是树的高度。 最坏情况下是:

O(n)

这题和“二叉树的最大深度”有什么不同?

它们都用递归,但思想差别很大。

最大深度

最大深度这类题,递归返回值往往就是问题本身:

当前节点的最大深度 = max(左深度, 右深度) + 1

最大路径和

这题不是。 因为“完整路径答案”可能在当前节点这里左右同时展开,但返回给父节点时又不能两边都带上。

所以这题多了一层区分:

  • 一个值用于更新全局答案

  • 一个值用于递归返回

这也是它比普通树题更难的原因。


总结

这道题的核心思想可以概括成一句话:

递归计算每个节点向父节点提供的最大单边贡献,同时把“左贡献 + 当前节点 + 右贡献”作为经过当前节点的完整路径和,用来更新全局最大答案。

具体步骤就是:

  1. 递归得到左右子树对当前节点的最大贡献值

  2. 左贡献 + 右贡献 + 当前节点值 更新全局答案

  3. 返回 当前节点值 + max(左贡献, 右贡献) 作为单边贡献

  4. 如果单边贡献小于 0,就直接返回 0

是一道非常经典的树形 DP / 树形递归题。 真正吃透它之后,你会更容易理解很多后续题目里的共同套路:
  • 递归函数返回的未必是最终答案

  • 局部最优值和全局最优值可能要分开维护

  • 负贡献要及时截断

  • 树上的路径问题,经常要区分“单边延伸”和“当前成环节点评估”

把这道题真正理解透,再去看很多二叉树路径类问题时,你会明显更容易抓住递归定义。 因为“二叉树中的最大路径和”本身,就是这类题里最经典的一道模板题。

如果对您有帮助的话,能否支持一下博主?
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇