题目描述

✅ 124. 二叉树中的最大路径和

image-20260928190329798

image-20260928190329799

题意分析

在一棵非空二叉树中选择一条非空路径,使路径上所有节点值的和最大,返回这个最大和。路径的相邻节点必须由一条父子边连接,同一个节点最多出现一次。

起点和终点可以是任意节点,不要求经过根,也不要求从根走到叶子;路径可以只有一个节点,也可以从某个节点的一侧子树走到它的另一侧。它必须是一条连续的路线,不能在一个节点处分出三条支路。节点值可以为负,全部为负时也必须选择至少一个节点,不能用空路径的和 0 作为答案。

解法:后序 DFS 计算单边贡献

核心思路

[!blue]

先区分两个问题:一是当前子树能向父节点提供多大的路径和,二是已经遇到的完整路径中哪个最好。它们不能使用同一个返回值,因为一条路径接上父节点后,当前节点最多还能向下接一个孩子;如果左右孩子都接,再接父节点,就会在当前节点处分叉。

定义 gain(node) 为:必须包含 node,从它开始向下走、至多选择一侧子树时能得到的最大和。这个值允许为负,因为当前节点必须被包含。先递归求出左右孩子的贡献,再分别取 left = max(0, gain(node.left))、right = max(0, gain(node.right));负贡献只会降低总和,可以选择不连接那一侧。

当前节点向父节点返回的贡献是 node.val + max(left, right),只能选左右两侧中较好的一侧。另一方面,如果完整路径就在当前节点处向两边展开、不再接父节点,那么左右两侧都可以使用,候选答案就是 node.val + left + right。用它更新全局最大值 best,再返回单边贡献。

任意合法路径都有一个离根最近的节点。以这个节点为分界,路径最多包含左子树中的一条向下路线、节点本身,以及右子树中的一条向下路线;恰好对应上述候选形式。遍历所有节点,相当于枚举了所有可能的分界点,因此也能找到完全位于某棵子树中的最优路径。

左右贡献必须先计算完,当前节点才能合并,所以采用后序 DFS。best 初始化为根节点值,每个候选都包含当前真实节点,保证全负树仍会选出最大的节点值。被截为零的是可不选的子树贡献,不能把整条路径也当成可空选。

解题步骤

  1. 用 root.val 初始化 best,随后从根节点调用 gain。
  2. 遇到空节点返回 0,表示这一侧没有可连接的节点。
  3. 递归计算左右孩子的贡献,并分别将负贡献置为 0。
  4. 用 node.val + left + right 更新 best,表示当前节点作为路径最高点时的最大路径和。
  5. 返回 node.val + max(left, right),供父节点连接当前节点和其中一侧路径。
  6. 遍历完成后返回 best,而不是根节点的单边贡献。

代码实现

class Solution {
    private int best;

    public int maxPathSum(TreeNode root) {
        // 路径不能空选,每次调用都用真实节点重置全局答案。
        best = root.val;
        gain(root);

        return best;
    }

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

        // 负的子树贡献不接入当前路径,两侧分别截断到零。
        int left = Math.max(0, gain(node.left));
        int right = Math.max(0, gain(node.right));

        // 当前节点作路径最高点时,可以连接左右两条贡献。
        best = Math.max(best, node.val + left + right);

        // 向父节点延伸只能选一侧,否则会形成分叉。
        return node.val + Math.max(left, right);
    }
}
func maxPathSum(root *TreeNode) int {
    // 路径不能空选,答案初值必须来自真实节点。
    best := root.Val

    var gain func(*TreeNode) int
    gain = func(node *TreeNode) int {
        if node == nil {
            return 0
        }

        left, right := gain(node.Left), gain(node.Right)
        // 负的子树贡献不接入当前路径,两侧分别截断到零。
        if left < 0 {
            left = 0
        }
        if right < 0 {
            right = 0
        }
        // 当前节点作路径最高点时,可以连接左右两条贡献。
        if sum := node.Val + left + right; sum > best {
            best = sum
        }
        if right > left {
            left = right
        }
        // 向父节点延伸只能选一侧,否则会形成分叉。
        return node.Val + left
    }

    gain(root)
    return best
}

复杂度分析

  • 时间复杂度:$O(n)$,n 为节点数。每个节点只进行一次左右贡献合并和答案更新。
  • 空间复杂度:$O(h)$,h 为树高,来自递归调用栈。平衡树为 $O(\log n)$,退化成链时为 $O(n)$。

关键点总结

[!green]

  • 返回给父节点的是以当前节点为端点的单边贡献,全局答案记录可以连接两侧的完整路径。
  • 每条路径都有唯一的最高节点,逐节点合并左右贡献就能覆盖所有候选路径。
  • 可以舍弃负的子树贡献,但每个候选必须包含当前节点;用真实节点值初始化答案。

易错点总结

[!yellow]

  • 把 best 初始化为 0,会让全负树错误地返回不存在的空路径。
  • 将 node.val + left + right 返回给父节点,会把三条支路拼在一起,不再是合法路径。
  • 只使用根节点的返回值,会漏掉不经过根的路径,也会漏掉在某个节点处连接左右两侧的路径。
  • 左右贡献需要分别与 0 取最大值;直接把负贡献一起相加,会让本可不连接的子树降低答案。
  • Java 中 best 是成员变量,必须在每次 maxPathSum 调用时重置,避免同一对象处理下一棵树时沿用旧结果。

相似题目

题目 难度 关联与区别
543. 二叉树的直径 简单 同样在节点处组合左右子路径,并只向父节点返回单侧贡献;原题计边数,本题累加节点值。
687. 最长同值路径 中等 同样寻找可能跨过某节点的最长路径,原题要求相邻节点同值,本题按权值和取最优。
104. 二叉树的最大深度 简单 后序返回子树高度并在根处合并信息;本题舍弃负贡献后合并最大路径和,该题取两侧最大高度加一。
110. 平衡二叉树 简单 后序返回子树高度并在根处合并信息;本题舍弃负贡献后合并最大路径和,该题额外验证两侧高度差。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/91304980
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!