Leetcode Hot 100 继续推进,这一题轮到二叉树里的“高压线选手”:二叉树中的最大路径和。
这题看着像普通二叉树递归, 实际上很容易把人绕进去。
因为它的路径定义有几个关键特点:
- 路径不一定从根开始
- 路径不一定在叶子结束
- 路径上节点不能重复
- 但路径必须连续
也就是说, 它找的不是“从根往下的最大值”, 而是:
整棵树中任意一条合法路径的最大路径和。
这题真正的难点,不在递归本身, 而在于你必须想明白:
递归返回值到底表示什么。
题目链接
题目描述
给你一个二叉树的根节点 root,返回其 最大路径和。
路径被定义为一条从树中任意节点出发,沿父子关系连接到任意节点的序列。 同一个节点在一条路径中至多出现一次。
路径至少包含一个节点。

解题思路:递归 + 单链贡献
这题最核心的思想是:
更新全局答案时,可以把当前节点当成拐点;但递归返回给父节点时,只能返回一条单链。
这句话是整题灵魂,必须背下来。
为什么返回值只能是“单链”
假设我们定义:
dfs(node)表示“从当前节点出发,向下延伸,所能得到的最大路径和”。
注意这里的路径,是要返回给父节点继续使用的。
那它就不能同时带上:
- 左子树一条路径
- 当前节点
- 右子树一条路径
因为如果这样返回给父节点,路径就分叉了。 而题目要求的路径必须是一条线,不能长成三叉路口。
所以 dfs(node) 返回的东西只能是下面三种之一:
- 只取当前节点
node.val - 当前节点 + 左边单链
- 当前节点 + 右边单链
换句话说:
递归返回值表示的是“当前节点向下的一条最大单链贡献”。
当前节点如何更新全局答案
虽然返回给父节点时不能左右都带着, 但在“当前节点本地结算”的时候,可以把当前节点当作一条路径的最高点。
这样它就可以同时连接:
- 左边一条向下链
- 当前节点
- 右边一条向下链
于是当前节点作为拐点时,能形成的路径和就是:
left_val + node.val + right_val这就是为什么这题要维护一个全局变量 self.ans。
它表示:
遍历到目前为止,整棵树里出现过的最大路径和。
为什么负贡献要直接丢掉
如果某棵子树返回的是负数,
比如 -5,那把它接到当前节点上,只会拖后腿。
对于求最大路径和来说:
- 正贡献可以留
- 负贡献不如不要
所以我们在递归里会写:
left_val = max(dfs(node.left), 0)right_val = max(dfs(node.right), 0)意思非常直接:
子树要是不给力,就让它原地待命,不许上桌。
代码实现
class Solution(object): def maxPathSum(self, root): """ :type root: Optional[TreeNode] :rtype: int """ self.ans = -float('inf')
def dfs(node): if not node: return 0
left_val = max(dfs(node.left), 0) right_val = max(dfs(node.right), 0)
# 当前节点作为拐点 self.ans = max(self.ans, left_val + node.val + right_val)
# 返回给父节点的只能是一条单链 return node.val + max(left_val, right_val)
dfs(root) return self.ans代码解析
1. 为什么空节点返回 0
if not node: return 0空节点没有贡献,
返回 0 就表示“这条路你可以不选”。
后面再配合:
max(dfs(node.left), 0)就能自然完成“负数贡献舍弃”的逻辑。
2. 先算左右子树的最大单链贡献
left_val = max(dfs(node.left), 0)right_val = max(dfs(node.right), 0)这里的 left_val、right_val 表示的是:
- 左子树能给当前节点提供的最大单链收益
- 右子树能给当前节点提供的最大单链收益
如果收益是负数,直接按 0 处理。
3. 为什么更新答案时能左右都拿
self.ans = max(self.ans, left_val + node.val + right_val)因为此时我们是在“当前节点本地结算”, 把它看作一条路径的拐点。
左边过来一条链, 右边再出去一条链, 是合法的。
所以这一刻允许“左右通吃”。
4. 为什么返回给父节点时只能选一边
return node.val + max(left_val, right_val)因为父节点如果还要继续拼接路径, 当前节点只能提供一条向下的链。
要么带左边, 要么带右边, 不能左右都带着一起上交。
不然路径就分叉了,题目不认。
你的原始思路为什么也能做
你原来的写法是:
self.ans = max(self.ans, left_val+node.val+right_val, left_val+node.val, right_val+node.val, node.val)return max(left_val, right_val, 0) + node.val这个逻辑本质上也是对的。
因为你在更新答案时,把可能情况都枚举了一遍:
- 只取自己
- 自己 + 左边
- 自己 + 右边
- 自己 + 左右两边
它能过,也没错。
只不过把负贡献提前裁掉之后,代码可以进一步压缩成标准写法:
left_val = max(dfs(node.left), 0)right_val = max(dfs(node.right), 0)self.ans = max(self.ans, left_val + node.val + right_val)return node.val + max(left_val, right_val)这样更简洁,也更容易讲清楚。
示例理解
假设当前节点值是 10,
左右子树递归返回值分别是:
left_val = 9right_val = 15那么:
当前节点作为拐点更新答案
9 + 10 + 15 = 34这表示一条完整路径:
- 左边单链
- 当前节点
- 右边单链
返回给父节点时
只能返回:
10 + max(9, 15) = 25因为要继续往上接,只能保留一边。
这就是“能拐弯更新答案,但不能拐弯返回父节点”的区别。
复杂度分析
时间复杂度
每个节点只访问一次, 所以时间复杂度为:
O(n)其中 n 是节点总数。
空间复杂度
递归栈深度取决于树高, 所以空间复杂度为:
O(h)其中 h 是树的高度。
最坏情况下树退化成链表,则为 O(n)。
易错点总结
1. 把返回值理解成“当前子树最大路径和”
这是最常见的误区。
dfs(node) 返回的不是:
当前整棵子树里的最大路径和
而是:
当前节点向下延伸的一条最大单链和
真正的“最大路径和”要靠全局变量 self.ans 维护。
2. 返回给父节点时把左右两边都加上
错误写法通常像这样:
return left_val + node.val + right_val这样会让路径分叉,不符合题意。
3. 没有处理负数贡献
如果不把负数裁掉, 就可能把一条本来不错的路径硬生生拖垮。
记住:
负贡献不如不要。
4. 以为路径必须经过根节点
这题的最大路径完全可能出现在某棵子树内部, 根节点甚至可能只是个路人甲。
所以必须对每个节点都尝试“当一次拐点”。
小结
这题的核心其实就一句:
每个节点都尝试作为路径拐点更新答案;递归只向父节点返回最大单链贡献。
也可以记成更顺口的一句:
更新答案时左右通吃,返回父节点时单边上交。
把这层想清楚, 这题就不再是“最大路径和玄学”, 而是一道很标准的树形递归题。
它表面是在问最大路径, 实际上是在考你:
能不能分清“局部最优返回值”和“全局最优答案”不是一回事。
会了这题,二叉树递归就算真的开始有点火候了。🦐