题目描述
二叉树中的路径被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中至多出现一次。该路径至少包含一个节点,且不一定经过根节点。
路径和是路径中各节点值的总和。
给你一个二叉树的根节点 root,返回其最大路径和。
示例 1:
输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6
树结构:
1
/ \
2 3
路径 2→1→3:2+1+3=6
示例 2:
输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42
树结构:
-10
/
9
\
20
/ \
15 7
路径 15→20→7:15+20+7=42
提示:
- 树中节点数目范围是 [1, 3 * 10^4]
- -1000 <= Node.val <= 1000
解题思路总览
| 方法 | 思路 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|---|
| 后序遍历 + 最大贡献值 | 计算每个节点能为父路径提供的最大贡献,O(1) 更新全局最大 | O(n) | O(h) | 面试首选 |
核心原理:
- 最大路径和 = 节点值 + max(0, 左子树最大贡献) + max(0, 右子树最大贡献)
- 最大贡献值 = max(0, 节点值 + max(左贡献, 右贡献)),只能选一条边往上走
后序遍历 + 最大贡献值(推荐)
思路
这是一道经典的后序遍历变形题,核心思想:
-
最大路径和:路径可以从任意节点开始,到任意节点结束。但经过某个节点的最大路径和 = 节点值 + 左子树最大贡献 + 右子树最大贡献(左右贡献取正的)
-
最大贡献值:对于父节点来说,子节点只能"贡献"一条路径上来的值。所以一个节点能贡献给父节点的最大值 = max(0, 节点值 + max(左贡献, 右贡献)),因为贡献必须往上走,不能分叉
-
全局记录 :用
ans记录所有节点作为"拐点"时的最大路径和
完整代码
cpp
/**
* 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 {
public:
int ans = INT_MIN; // 全局最大路径和
int dfs(TreeNode* node) {
if (!node) return 0; // 空节点贡献为 0
// 递归计算左右子树的贡献值(如果为负则取 0,表示不往下走)
int l = dfs(node->left);
int r = dfs(node->right);
// 更新全局最大路径和:
// 路径可以是:左子树 → node → 右子树,或者只有其中一边,或者只有 node 自己
ans = max(ans, l + r + node->val);
// 返回该节点能为父路径贡献的最大值(只能选一条路径往上走)
// 如果贡献是负数,则不贡献(取 0)
return max(0, max(l, r) + node->val);
}
int maxPathSum(TreeNode* root) {
dfs(root);
return ans;
}
};
算法流程图
以示例 2 为例,root = [-10,9,20,null,null,15,7]:
树结构:
-10
/
9
\
20
/ \
15 7
后序遍历过程:
Step 1: dfs(15)
l = dfs(null) = 0
r = dfs(null) = 0
ans = max(-inf, 0+0+15) = 15
return max(0, max(0,0)+15) = 15
Step 2: dfs(7)
l = dfs(null) = 0
r = dfs(null) = 0
ans = max(15, 0+0+7) = 15
return max(0, max(0,0)+7) = 7
Step 3: dfs(20)
l = dfs(15) = 15
r = dfs(7) = 7
ans = max(15, 15+7+20) = max(15, 42) = 42
return max(0, max(15,7)+(-10)) = max(0, 15-10) = 5
Step 4: dfs(9)
l = dfs(null) = 0
r = dfs(20) = 5
ans = max(42, 0+5+9) = max(42, 14) = 42
return max(0, max(0,5)+9) = 14
Step 5: dfs(-10)
l = dfs(9) = 14
r = dfs(null) = 0
ans = max(42, 14+0+(-10)) = max(42, 4) = 42
return max(0, max(14,0)+(-10)) = max(0, 4) = 4
最终 ans = 42
逐行解析
cpp
int ans = INT_MIN; // 全局最大路径和
- 初始化为
INT_MIN,因为节点值可能都是负数。 ans记录遍历过程中遇到的最大路径和。
cpp
int dfs(TreeNode* node) {
if (!node) return 0; // 空节点贡献为 0
- 递归终止条件:空节点返回 0,表示没有贡献。
cpp
int l = dfs(node->left);
int r = dfs(node->right);
- 递归计算左子树和右子树的最大贡献值。
- 这里用的是后序遍历(左右根),确保先处理子树,再处理根。
cpp
ans = max(ans, l + r + node->val);
- 更新全局最大路径和:
l + r + node->val表示经过当前节点的路径和(左边贡献 + 根 + 右边贡献)- 这种路径的特点是:拐点在这里,可以同时走左右两边
- 注意:
l和r可能是负数,但在计算路径和时,负数会拉低总和,所以不会影响最大值
cpp
return max(0, max(l, r) + node->val);
- 返回该节点对父路径的最大贡献:
- 只能选一条路径(左边或右边),所以取
max(l, r) - 加上当前节点值
node->val - 如果结果为负数,返回 0 表示不贡献(路径没必要往上走)
复杂度分析
| 复杂度 | 值 | 说明 |
|---|---|---|
| 时间 | O(n) | 每个节点访问一次 |
| 空间 | O(h) | 递归栈深度,h 为树高 |
优点: 一次遍历搞定,时间效率高
缺点: 需要理解"贡献值"的概念
关键概念区分
| 概念 | 定义 | 用途 |
|---|---|---|
| 最大路径和 | 经过树中任意节点的最大路径的和 | 最终答案 |
| 最大贡献值 | 一个节点能为父路径贡献的最大值(只能走一条边往上) | 递归返回值 |
为什么贡献值只能选一条边?
因为父路径只能从一个方向上来,不能同时从两边拐上来再往上走。所以一个节点对父节点的贡献,只能选择左子树或右子树中贡献较大的那个。
面试追问 FAQ
| 问题 | 解答 |
|---|---|
Q1:为什么 l + r + node->val 能代表经过当前节点的最大路径和? |
因为后序遍历保证了左右子树的贡献值 l 和 r 都已经是各自子树中能贡献给父节点的最大值。当路径"经过"当前节点时,可以同时吸收左子树和右子树的贡献(因为路径可以在当前节点拐弯)。所以 l + r + node->val 就是以当前节点为拐点的最大路径和。 |
Q2:为什么要用 max(0, ...) 限制贡献值? |
因为路径可以"不走"某个子树。如果某个子树对路径的贡献是负数,那还不如不选这条路径,直接把贡献值设为 0 即可。这确保了贡献值永远是非负的,不会拉低路径和。 |
Q3:为什么全局 ans 初始值是 INT_MIN? |
因为节点值可能是负数。如果树中只有一个节点且值为负,ans 应该就是这个负数。所以需要用一个很小的初始值来确保正确答案能被正确更新。 |
Q4:为什么返回值是 max(l, r) + node->val 而不是 l + r + node->val? |
因为返回值表示的是"该节点对父节点的贡献"。父路径只能从一边上来(从上往下走),不能同时从两边拐。所以只能选 max(l, r) 而不是 l + r。 |
Q5:为什么返回值要取 max(0, ...)? |
如果 max(l, r) + node->val 是负数,说明当前节点对任何路径都是"拖后腿"的,父路径不应该包含这个节点。所以返回 0 表示"不贡献",相当于路径在这里结束或根本不走这条分支。 |
| Q6:这道题和 437 题(路径总和 III)有什么区别? | 437 题要求路径必须向下(父子关系),但可以有多个起点和终点。本题更通用,路径可以是任意形状(可以拐弯)。437 题用前缀和 + HashMap 解决;本题用后序遍历 + 贡献值解决。 |
相关题目
| 题目 | 难度 | 关键点 |
|---|---|---|
| 124. 二叉树中的最大路径和 | 困难 | 后序遍历,贡献值 |
| 437. 路径总和 III | 中等 | 前缀和,HashMap |
| 112. 路径总和 | 简单 | 从根到叶子的路径和 |
| 129. 求根到叶子节点数字之和 | 中等 | 路径遍历变形 |
总结
| 要点 | 说明 |
|---|---|
| 核心原理 | 后序遍历计算每个节点的贡献值,O(1) 更新全局最大路径和 |
| 最大路径和 | ans = max(ans, l + r + node->val),经过当前节点的路径和 |
| 最大贡献值 | return max(0, max(l, r) + node->val),只能选一条边往上走 |
| 为什么要 max(0,...) | 贡献值不能为负,否则会拉低路径和,直接取 0 即可 |
| 时间复杂度 | O(n),每个节点访问一次 |
| 空间复杂度 | O(h),递归栈深度 |