【算法3】二叉树中的最大路径和

二叉树中的最大路径和

给定一棵非空二叉树,路径由相邻节点组成,每个节点最多经过一次。路径不必经过根节点,也不必从叶子节点开始或结束。

例如,在下面的二叉树中,最大路径是 15 → 20 → 7,最大路径和是 15 + 20 + 7 = 42。

text 复制代码
        -10
        /  \
       9    20
           /  \
          15   7

一、解题思路

当前节点必须先得到左右子树的计算结果,所以采用"左子树、右子树、当前节点"的后序遍历。

递归函数 maxGain(root) 的返回值不是整棵树的最大路径和,而是"从当前节点出发,只向一侧延伸时,能够返回给父节点的最大路径和"。

对于当前节点,左、右子树的贡献分别为 leftGain 和 rightGain。如果某个贡献是负数,连接这棵子树只会使路径和变小,因此通过 Math.max(0, 子树贡献) 将它舍弃。

一条路径如果在当前节点处结束,可以同时连接左右两侧,所以用 root.val + leftGain + rightGain 更新全局变量 maxSum。

但当前节点向父节点返回时,路径还要继续向上连接,不能同时保留左右两侧,否则路径会产生分叉。因此只能返回 root.val + Math.max(leftGain, rightGain)。

以节点 20 为例,左右子树分别返回 15 和 7。经过节点 20 的完整路径和是 15 + 20 + 7 = 42,所以用 42 更新 maxSum;而节点 20 返回给父节点时,只能选择较大的一侧,因此返回 20 + max(15, 7) = 35。

两种结果的职责不同:

text 复制代码
root.val + leftGain + rightGain
用于更新整棵树的最大路径和

root.val + max(leftGain, rightGain)
用于返回给父节点继续计算

全局变量 maxSum 必须初始化为 Integer.MIN_VALUE。如果整棵树都是负数,最大路径和应当是其中最大的节点值,而不是 0。

二、完整代码

java 复制代码
public class BinaryTreeMaximumPathSum {

    static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;

        TreeNode(int val) {
            this.val = val;
        }
    }

    /**
     * 遍历过程中发现的最大完整路径和。
     */
    private int maxSum;

    public int maxPathSum(TreeNode root) {
        maxSum = Integer.MIN_VALUE;

        // 启动后序遍历,递归过程中会更新 maxSum。
        maxGain(root);

        return maxSum;
    }

    /**
     * 返回从当前节点出发,只向一侧延伸时,
     * 能够提供给父节点的最大路径和。
     */
    private int maxGain(TreeNode root) {
        if (root == null) {
            return 0;
        }

        int leftGain = Math.max(
                0,
                maxGain(root.left)
        );

        int rightGain = Math.max(
                0,
                maxGain(root.right)
        );

        // 完整路径可以同时连接左右两侧。
        int currentPathSum =
                root.val + leftGain + rightGain;

        maxSum = Math.max(
                maxSum,
                currentPathSum
        );

        // 返回父节点时只能选择一侧。
        return root.val + Math.max(
                leftGain,
                rightGain
        );
    }

    public static void main(String[] args) {
        /*
         *          -10
         *          /  \
         *         9    20
         *             /  \
         *            15   7
         */
        TreeNode root = new TreeNode(-10);

        root.left = new TreeNode(9);
        root.right = new TreeNode(20);
        root.right.left = new TreeNode(15);
        root.right.right = new TreeNode(7);

        BinaryTreeMaximumPathSum solution =
                new BinaryTreeMaximumPathSum();

        int result = solution.maxPathSum(root);

        System.out.println("最大路径和:" + result);
    }
}

程序输出:

text 复制代码
最大路径和:42

每个节点只计算一次,所以时间复杂度为 O(n);递归调用栈的最大深度等于树高,所以空间复杂度为 O(h)。

三、压栈、出栈与返回父节点

程序首先进入 maxPathSum(-10),将 maxSum 初始化为最小整数,然后调用 maxGain(-10)。此时 maxPathSum 暂停,等待整棵树递归结束。

text 复制代码
栈顶
┌──────────────────────────┐
│ maxGain(-10)              │
├──────────────────────────┤
│ maxPathSum:等待递归结束  │
└──────────────────────────┘
栈底

maxGain(-10) 先计算左子树,因此调用 maxGain(9)。节点 -10 的栈帧不会消失,而是留在栈中等待左子树返回。

text 复制代码
栈顶
┌──────────────────────────┐
│ maxGain(9)                │
├──────────────────────────┤
│ maxGain(-10):等待左结果  │
├──────────────────────────┤
│ maxPathSum:等待递归结束  │
└──────────────────────────┘
栈底

节点 9 的左右子树都是空节点,所以 leftGain = 0、rightGain = 0。经过节点 9 的完整路径和为 9 + 0 + 0 = 9,因此将 maxSum 更新为 9;随后,节点 9 向父节点 -10 返回 9 + max(0, 0) = 9,并从调用栈中弹出。

text 复制代码
栈顶
┌──────────────────────────┐
│ maxGain(-10)              │
│ leftGain = 9              │
├──────────────────────────┤
│ maxPathSum:等待递归结束  │
└──────────────────────────┘
栈底

maxSum = 9

得到左子树结果后,maxGain(-10) 调用 maxGain(20) 计算右子树。节点 20 又先调用 maxGain(15),因此栈中依次保存根节点和正在等待结果的父节点。

text 复制代码
栈顶
┌──────────────────────────┐
│ maxGain(15)               │
├──────────────────────────┤
│ maxGain(20):等待左结果   │
├──────────────────────────┤
│ maxGain(-10):等待右结果  │
├──────────────────────────┤
│ maxPathSum:等待递归结束  │
└──────────────────────────┘
栈底

节点 15 没有孩子,所以经过它的完整路径和是 15,maxSum 从 9 更新为 15。节点 15 向父节点 20 返回 15,然后出栈。

回到节点 20 后,leftGain = 15。接着调用 maxGain(7) 计算右子树:

text 复制代码
栈顶
┌──────────────────────────┐
│ maxGain(7)                │
├──────────────────────────┤
│ maxGain(20):等待右结果   │
├──────────────────────────┤
│ maxGain(-10):等待右结果  │
├──────────────────────────┤
│ maxPathSum:等待递归结束  │
└──────────────────────────┘
栈底

节点 7 没有孩子,所以向父节点 20 返回 7,然后出栈。此时节点 20 已经得到 leftGain = 15 和 rightGain = 7。

text 复制代码
栈顶
┌──────────────────────────┐
│ maxGain(20)               │
│ leftGain = 15             │
│ rightGain = 7             │
├──────────────────────────┤
│ maxGain(-10):等待右结果  │
├──────────────────────────┤
│ maxPathSum:等待递归结束  │
└──────────────────────────┘
栈底

经过节点 20 的完整路径和是 15 + 20 + 7 = 42,所以 maxSum 更新为 42。节点 20 向父节点 -10 返回时只能选择一侧,因此返回 20 + max(15, 7) = 35,然后出栈。

回到根节点 -10 后,左右子树的返回值分别是 9 和 35。

text 复制代码
栈顶
┌──────────────────────────┐
│ maxGain(-10)              │
│ leftGain = 9              │
│ rightGain = 35            │
├──────────────────────────┤
│ maxPathSum:等待递归结束  │
└──────────────────────────┘
栈底

maxSum = 42

经过根节点的完整路径和是 9 + (-10) + 35 = 34,小于当前的 42,所以 maxSum 不变。

根节点还会计算单边返回值 -10 + max(9, 35) = 25,但根节点没有父节点,所以这个返回值没有被使用。maxGain(-10) 出栈后,程序回到 maxPathSum。

text 复制代码
栈顶
┌──────────────────────────┐
│ maxPathSum                │
│ maxSum = 42               │
└──────────────────────────┘
栈底

最后,maxPathSum 返回 maxSum,结果为 42。

整个过程只有一条数据链:

text 复制代码
子节点返回单边贡献
        ↓
父节点组合左右贡献
        ↓
更新全局最大路径和
        ↓
父节点再返回自己的单边贡献

maxGain 的返回值供父节点继续计算,maxSum 记录遍历过程中出现的最大完整路径,maxPathSum 在递归结束后返回最终结果。

相关推荐
朝朝辞暮i3 小时前
C++ 第 21 课:struct —— 把一组相关数据打包在一起
开发语言·c++·算法
青梅橘子皮3 小时前
Blue---二分算法(二分查找与二分答案)
算法
长河4 小时前
Java的GC(垃圾回收)中,标记无用对象的核心方法是可达性分析算法
java·jvm·算法
月光船幽幽4 小时前
加性偏移外推提升参数识别可靠性
python·算法
Ivanqhz4 小时前
层归一化、残差、前馈网络与激活函数简述
服务器·数据库·人工智能·深度学习·算法
不会就选b5 小时前
算法日常・每日刷题--<动态规划>3
数据结构·算法
-dzk-6 小时前
【堆】LC 347.前 K 个高频元素
算法·堆
H.莓飛6 小时前
【数据结构】栈_OJ题
linux·数据结构·算法·centos
Zootopia6266 小时前
多架 eVTOL集群排班调度方案思考
人工智能·算法·数学建模·matlab·动态规划·无人机·evtol
和裕7 小时前
年度框架直供 vs 零散按需采购:定制纸箱采购成本、交付与服务核心区别全对比
大数据·运维·网络·人工智能·算法