二叉树中的最大路径和
二叉树中的路径被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。
同一个节点在一条路径序列中至多出现一次。
该路径至少包含一个节点,且不一定经过根节点。
路径和是路径中各节点值的总和。
给你一个二叉树的根节点 root,返回其最大路径和。
示例 1:
输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3,路径和为 2 + 1 + 3 = 6。
示例 2:
输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7,路径和为 15 + 20 + 7 = 42。
提示:
- 树中节点数目范围是
[1, 3 * 10^4] -1000 <= Node.val <= 1000
后序 DFS:
/**
* 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 ans;
public:
int maxPathSum(TreeNode* root) {
ans = INT_MIN;
gain(root);
return ans;
}
private:
int gain(TreeNode* node) {
if (node == nullptr)
{
return 0;
}
int leftGain = max(gain(node->left), 0);
int rightGain = max(gain(node->right), 0);
int pathSum = node->val + leftGain + rightGain;
ans = max(ans, pathSum);
return node->val + max(leftGain, rightGain);
}
};
核心思想
这题和“二叉树的直径”很像。
路径不一定经过根节点,所以不能只计算从根节点出发的最大路径。
对于任意一个节点,如果最大路径经过这个节点,那么路径可能是:
左子树中的某条向下路径 -> 当前节点 -> 右子树中的某条向下路径
所以需要在每个节点都尝试一次:
node->val + 左侧最大贡献 + 右侧最大贡献
并用它更新全局答案。
这题最关键的观察是:
对当前节点来说,更新全局答案时可以同时使用左右两边;但向父节点返回时,只能选择左边或右边其中一条路径。
原因是路径不能分叉。
如果当前节点要继续接到父节点上,那么这条路径只能是:
父节点 -> 当前节点 -> 左子树某条路径
或者:
父节点 -> 当前节点 -> 右子树某条路径
不能同时带着左右两边一起返回给父节点,否则到父节点后就会形成分叉,不再是一条合法路径。
最大贡献值的含义
定义函数:
int gain(TreeNode* node)
它表示:
从 node 出发,向下走到某个节点,能够提供给 node 父节点的最大路径和
这个值必须包含 node 自己。
并且它只能选择一条向下路径,不能同时选择左子树和右子树。
所以返回值是:
node->val + max(leftGain, rightGain)
其中:
leftGain:左孩子能向当前节点提供的最大贡献rightGain:右孩子能向当前节点提供的最大贡献
为什么负贡献要丢掉
如果某棵子树的最大贡献是负数,把它接到路径里只会让路径和变小。
例如当前节点值为 20,左子树最大贡献为 15,右子树最大贡献为 -5。
如果强行接上右子树,路径和会减少 5。
所以负贡献不如不选。
代码中写成:
int leftGain = max(gain(node->left), 0);
int rightGain = max(gain(node->right), 0);
这表示:
- 如果子树贡献为正,就接上它。
- 如果子树贡献为负,就当作贡献为
0,不接这条分支。
如何更新全局答案
对于当前节点,经过它的最大路径和是:
node->val + leftGain + rightGain
这里可以同时使用左侧和右侧。
因为这条路径的形状是:
左侧某个节点 -> 当前节点 -> 右侧某个节点
它没有分叉,是一条合法路径。
代码中用:
int pathSum = node->val + leftGain + rightGain;
ans = max(ans, pathSum);
更新全局最大路径和。
注意,更新答案和返回贡献是两件不同的事:
- 更新答案:可以把当前节点当作路径最高点,同时连接左右两边。
- 返回贡献:只能返回一边给父节点继续使用。
为什么使用后序遍历
计算当前节点的最大路径和,需要先知道左右子树能提供的最大贡献。
也就是说,必须先处理:
左子树
右子树
再处理当前节点。
这正是后序遍历的顺序。
代码中先递归:
int leftGain = max(gain(node->left), 0);
int rightGain = max(gain(node->right), 0);
然后再用左右贡献更新当前节点的路径和。
为什么 ans 要初始化为 INT_MIN
树中节点值可能全是负数。
例如:
root = [-3]
最大路径至少要包含一个节点,所以答案应该是 -3,而不是 0。
如果把 ans 初始化为 0,就会错误地返回 0。
因此要初始化为:
ans = INT_MIN;
这样即使所有路径和都是负数,也能正确更新到最大的那个节点值。
边界情况
如果二叉树只有一个节点,最大路径就是这个节点本身。
如果所有节点值都是负数,最大路径和就是值最大的那个单独节点。
如果某个子树贡献为负数,算法会把它当作 0,表示不选择这条分支。
如果最大路径完全位于左子树或右子树中,不经过根节点,算法仍然会在递归处理那棵子树时更新全局答案。
如果最大路径经过某个节点并连接它的左右子树,算法会在处理这个节点时用 leftGain + node->val + rightGain 更新答案。
正确性证明
我们证明:算法返回的 ans 等于二叉树中的最大路径和。
结论 1:gain(node) 正确返回当前节点能向父节点提供的最大贡献
对于空节点,返回 0,表示没有贡献。
对于非空节点,向父节点提供的路径必须从当前节点出发,并且只能继续走向左子树或右子树中的一边。
如果同时选择左右两边,再接到父节点上,路径就会分叉,不再是一条合法路径。
因此最大贡献只能是:
node->val + max(leftGain, rightGain)
其中负数贡献会被当作 0,表示不选择这条子路径。
所以 gain(node) 的返回值正确。
结论 2:经过当前节点的最大路径和会被正确计算
对于当前节点,如果一条路径以当前节点作为最高连接点,那么它最多可以包含三部分:
- 左子树向当前节点提供的一条最大贡献路径
- 当前节点本身
- 右子树向当前节点提供的一条最大贡献路径
负贡献不会让路径变优,因此可以被丢掉。
所以经过当前节点的最大路径和是:
node->val + leftGain + rightGain
算法正是用这个值更新全局答案。
结论 3:所有可能的最大路径都会被考虑
任意一条合法路径在二叉树中都有一个最高节点。
这个最高节点可能是路径的一端,也可能位于路径中间,连接左右两个方向。
当 DFS 处理到这个最高节点时,路径左右两侧分别位于它的左子树和右子树中,或者只有一侧存在。
根据结论 1,左右子树能提供的最优向下贡献已经被正确计算。
根据结论 2,算法会在这个最高节点处计算包含它的最大路径和。
因此任意可能成为最优解的路径,都会在它的最高节点处被考虑到。
结论 4:算法不会得到非法路径和
每次更新 ans 的路径形态都是:
左侧某条向下路径 -> 当前节点 -> 右侧某条向下路径
其中左侧和右侧都来自当前节点的不同子树,且每边最多选择一条向下路径。
这样的节点序列中不会重复经过同一个节点,也不会产生分叉。
因此每次用于更新 ans 的值都对应一条真实存在的合法路径。
得出结论
由结论 1 可知,递归返回的最大贡献值正确。
由结论 2 可知,每个节点作为路径最高点时的最大路径和计算正确。
由结论 3 可知,所有可能的最优路径都会被考虑,不会遗漏。
由结论 4 可知,算法不会用非法路径更新答案。
因此算法返回的 ans 就是整棵二叉树的最大路径和。
举例理解
以:
root = [-10,9,20,null,null,15,7]
为例,这棵树可以表示为:
-10
/ \
9 20
/ \
15 7
从底向上计算最大贡献:
| 节点 | 左贡献 | 右贡献 | 经过当前节点的路径和 | 返回给父节点的贡献 |
|---|---|---|---|---|
9 |
0 |
0 |
9 |
9 |
15 |
0 |
0 |
15 |
15 |
7 |
0 |
0 |
7 |
7 |
20 |
15 |
7 |
42 |
35 |
-10 |
9 |
35 |
34 |
25 |
最大路径和是 42。
对应路径为:
15 -> 20 -> 7
注意在节点 20 处,更新答案时可以使用左右两边:
15 + 20 + 7 = 42
但节点 20 返回给父节点 -10 的贡献只能选择更大的一边:
20 + max(15, 7) = 35
因为向上接到父节点时,路径不能同时带着左右两条分支。
复杂度分析
设二叉树节点数为 n,树高为 h。
每个节点只会被 DFS 访问一次。
每次访问只进行常数次计算。
所以时间复杂度是:
O(n)
递归调用栈的深度等于树高。
所以空间复杂度是:
O(h)
如果树退化成链,h = n;如果树比较平衡,h = log n。