【算法】124.二叉树中的最大路径和--通俗讲解

68 阅读6分钟

通俗易懂讲解“二叉树的最大路径和”算法题目

一、题目是啥?一句话说清

在二叉树中找一条路径(任意节点序列,每个节点最多有一个父节点和一个子节点),使得路径上所有节点的值之和最大,这条路径不一定经过根节点,但至少包含一个节点。

示例:

  • 输入:二叉树,节点值可能为负
  • 输出:最大路径和,例如在二叉树 [-10,9,20,null,null,15,7] 中,最大路径和是 42(路径 15 → 20 → 7)

二、解题核心

使用递归方法,从叶子节点开始向上计算每个节点能提供的最大贡献值,同时在每个节点处计算以该节点为顶点的路径和,并更新全局最大路径和。 这就像每个节点向父节点报告“我能为你提供的最大价值是多少”,而父节点则结合左右孩子的报告计算“如果路径以我为中心,总和能有多大”。

三、关键在哪里?(3个核心点)

想理解并解决这道题,必须抓住以下三个关键点:

1. 递归计算节点贡献值

  • 是什么:对于每个节点,递归计算以该节点为起点的最大路径和(即从该节点向下延伸的路径,只能选择左或右一条分支)。
  • 为什么重要:这提供了节点对父节点的贡献值,帮助父节点计算更长的路径。

2. 更新全局最大路径和

  • 是什么:在每个节点处,计算以该节点为顶点的路径和(即节点值 + 左子树贡献 + 右子树贡献),并用此更新全局最大值。
  • 为什么重要:最大路径和可能出现在任何节点为顶点的路径上,而不一定是根节点。

3. 处理负值贡献

  • 是什么:如果子树的贡献值为负,则选择不包含该子树(即贡献值取0),因为负贡献会减少总和。
  • 为什么重要:这确保了路径和不会被负值拖累,从而找到真正的最大值。

四、看图理解流程(通俗理解版本)

让我们用二叉树 [-10,9,20,null,null,15,7] 为例来可视化过程:

  1. 从叶子节点开始

    • 节点15:左贡献=0,右贡献=0,以15为顶点的路径和=15。报告给父节点的贡献值=max(15,0)=15。
    • 节点7:左贡献=0,右贡献=0,以7为顶点的路径和=7。报告贡献值=max(7,0)=7。
  2. 处理节点20

    • 左贡献=15(来自左子树15),右贡献=7(来自右子树7)。
    • 以20为顶点的路径和=20 + 15 + 7 = 42(这是全局最大路径和)。
    • 报告给父节点的贡献值=20 + max(15,7) = 20 + 15 = 35(因为只能选一条分支)。
  3. 处理节点9

    • 左贡献=0,右贡献=0(因为无子树),以9为顶点的路径和=9。
    • 报告贡献值=max(9,0)=9。
  4. 处理根节点-10

    • 左贡献=9(来自左子树9),右贡献=35(来自右子树20)。
    • 以-10为顶点的路径和=-10 + 9 + 35 = 34,但34 < 42,所以全局最大值仍是42。
    • 报告贡献值=-10 + max(9,35) = -10 + 35 = 25(但25>0,所以报告25?不,实际上如果贡献为负,应该返回0,但这里25是正数)。

注意:在计算贡献值时,如果贡献为负,我们返回0,表示不包含该分支。所以根节点-10的贡献值应该是 max(-10 + max(9,35), 0) = max(25,0)=25?但根据算法,我们直接计算节点值加左右贡献的较大值,但如果结果为负,则返回0。

实际上,在递归函数中,我们计算节点的贡献值时,如果左或右贡献为负,我们就不取(即取0),所以对于根节点-10,贡献值 = -10 + max(左贡献,0) + max(右贡献,0)?不对,递归函数返回的是从该节点向下延伸的最大路径和(只能选一条分支),所以对于根节点,贡献值 = -10 + max( max(左贡献,0), max(右贡献,0) )?但左贡献是9(正),右贡献是35(正),所以贡献值 = -10 + max(9,35) = 25?但25是正数,所以返回25。

但在更新全局最大值时,我们计算以节点为顶点的路径和:节点值 + max(左贡献,0) + max(右贡献,0)。对于根节点,就是 -10 + 9 + 35 = 34。

最终全局最大值是42,来自节点20。

五、C++ 代码实现(附详细注释)

#include <iostream>
#include <algorithm>
using namespace std;

// 二叉树节点定义
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 maxPathSum(TreeNode* root) {
        int maxSum = INT_MIN; // 初始化全局最大路径和为最小整数
        maxGain(root, maxSum);
        return maxSum;
    }
    
private:
    int maxGain(TreeNode* node, int& maxSum) {
        if (node == nullptr) return 0; // 空节点贡献值为0
        
        // 递归计算左右子树的贡献值,如果贡献值为负,则取0(表示不包含该子树)
        int leftGain = max(maxGain(node->left, maxSum), 0);
        int rightGain = max(maxGain(node->right, maxSum), 0);
        
        // 计算以当前节点为顶点的路径和(可能包含左右子树)
        int priceNewPath = node->val + leftGain + rightGain;
        
        // 更新全局最大路径和
        maxSum = max(maxSum, priceNewPath);
        
        // 返回当前节点的最大贡献值(只能选择左或右一条分支)
        return node->val + max(leftGain, rightGain);
    }
};

// 测试代码
int main() {
    // 构建示例二叉树: [-10,9,20,null,null,15,7]
    TreeNode* left = new TreeNode(9);
    TreeNode* right_left = new TreeNode(15);
    TreeNode* right_right = new TreeNode(7);
    TreeNode* right = new TreeNode(20, right_left, right_right);
    TreeNode* root = new TreeNode(-10, left, right);
    
    Solution solution;
    int result = solution.maxPathSum(root);
    cout << "最大路径和: " << result << endl; // 输出42
    
    // 释放内存(实际面试中可能不需要)
    delete right_left;
    delete right_right;
    delete right;
    delete left;
    delete root;
    
    return 0;
}

六、注意事项

  • 初始化全局最大值为INT_MIN:因为节点值可能为负,所以不能初始化为0。
  • 空节点处理:递归基返回0,表示空节点没有贡献。
  • 贡献值取非负:在计算左右贡献值时,如果为负则取0,避免负贡献减少总和。
  • 路径和计算:在以节点为顶点的路径和中,我们允许包含左右子树,因为路径可以穿过该节点向两边延伸。
  • 返回值:递归函数返回的是从该节点向下延伸的最大路径和(只能选一条分支),用于父节点计算。

七、总结

理解此题的关键在于:

  • 递归计算贡献值:每个节点报告它能提供的最大价值(只能选一条分支)。
  • 更新全局最大值:在每个节点处计算以该节点为顶点的完整路径和(可包含左右分支)。
  • 处理负值:负贡献被忽略(取0),以确保路径和最大化。

掌握这三点,你就能高效解决二叉树的最大路径和问题。这道题是二叉树递归的经典应用,考察了对递归和路径的理解。多练习几次,注意递归的细节,就能熟练运用。