目录
[一、题目 1:二叉树中的最大路径和(LeetCode 124)](#一、题目 1:二叉树中的最大路径和(LeetCode 124))
[1. 题目描述](#1. 题目描述)
[2. 核心思想(树形 DP)](#2. 核心思想(树形 DP))
[3. 解法实现](#3. 解法实现)
[(1)基础递归解法(时间 O (n),空间 O (h),h 为树高)](#(1)基础递归解法(时间 O (n),空间 O (h),h 为树高))
[4. 易错点](#4. 易错点)
[二、题目 2:二叉树的直径(LeetCode 543)](#二、题目 2:二叉树的直径(LeetCode 543))
[1. 题目描述](#1. 题目描述)
[2. 核心思想(树形 DP)](#2. 核心思想(树形 DP))
[3. 解法实现](#3. 解法实现)
[(1)基础递归解法(时间 O (n),空间 O (h))](#(1)基础递归解法(时间 O (n),空间 O (h)))
[4. 易错点](#4. 易错点)
[三、树形 DP 常见题型的核心模板](#三、树形 DP 常见题型的核心模板)
[1. 模板结构](#1. 模板结构)
[2. 通用代码框架](#2. 通用代码框架)
[3. 题型适配示例](#3. 题型适配示例)
[4. 适用场景](#4. 适用场景)
一、题目 1:二叉树中的最大路径和(LeetCode 124)
1. 题目描述
给定二叉树的根节点,返回树中任意路径的节点值之和的最大值。
- 路径定义:节点序列(相邻节点有边、节点不重复),至少含 1 个节点,可不过根。
- 示例:输入
[1,2,3],输出6(路径2→1→3,和为2+1+3)。
2. 核心思想(树形 DP)
通过后序遍历自底向上处理,核心是 "子树贡献→当前节点最优→向上返回贡献":
- 子树贡献 :左 / 右子树能提供的最大路径和(若子树贡献为负,取
0,相当于 "不选该子树"); - 当前节点最优:以当前节点为核心的路径和(左贡献 + 右贡献 + 当前节点值),用它更新全局最大和;
- 向上返回贡献:当前节点能向父节点提供的最大路径和(当前节点值 + 左 / 右贡献的较大值)------ 因为父节点的路径只能延伸一个分支。
3. 解法实现
(1)基础递归解法(时间 O (n),空间 O (h),h 为树高)
java
class Solution {
// 全局变量存最大路径和(初始为最小值,处理全负节点的情况)
private int maxSum = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
dfs(root);
return maxSum;
}
// 递归函数:返回当前节点向父节点提供的最大贡献
private int dfs(TreeNode node) {
if (node == null) return 0; // 空节点贡献为0
// 左/右子树的贡献(负贡献取0)
int leftContribution = Math.max(dfs(node.left), 0);
int rightContribution = Math.max(dfs(node.right), 0);
// 计算当前节点的最优路径和,更新全局最大值
int currentMax = leftContribution + rightContribution + node.val;
maxSum = Math.max(maxSum, currentMax);
// 向父节点返回的贡献(只能选一个分支)
return node.val + Math.max(leftContribution, rightContribution);
}
}
(2)优化:迭代后序遍历(避免递归栈溢出)
当树深度极大(如 1e5)时,递归会栈溢出,用迭代模拟后序遍历:
java
class Solution {
public int maxPathSum(TreeNode root) {
if (root == null) return 0;
int maxSum = Integer.MIN_VALUE;
Deque<TreeNode> stack = new LinkedList<>();
// 存储每个节点的贡献(避免重复计算)
Map<TreeNode, Integer> contributionMap = new HashMap<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.peek();
// 先处理左子树(未处理过则压栈)
if (node.left != null && !contributionMap.containsKey(node.left)) {
stack.push(node.left);
}
// 再处理右子树(未处理过则压栈)
else if (node.right != null && !contributionMap.containsKey(node.right)) {
stack.push(node.right);
}
// 左右子树都处理完,计算当前节点的贡献
else {
stack.pop();
int left = node.left == null ? 0 : Math.max(contributionMap.get(node.left), 0);
int right = node.right == null ? 0 : Math.max(contributionMap.get(node.right), 0);
// 更新全局最大和
maxSum = Math.max(maxSum, left + right + node.val);
// 存储当前节点的贡献
contributionMap.put(node, node.val + Math.max(left, right));
}
}
return maxSum;
}
}
4. 易错点
- 全局变量初始化为
0:若树全是负节点(如[-3,-2,-1]),会错误返回0,需初始化为Integer.MIN_VALUE; - 返回贡献时包含左右分支:向父节点返回的贡献必须是 "当前节点 + 单个子树贡献",否则父节点无法延伸路径。
二、题目 2:二叉树的直径(LeetCode 543)
1. 题目描述
给定二叉树的根节点,返回树中任意两节点间路径的边数的最大值。
- 路径可不过根,边数 = 路径节点数 - 1;
- 示例:输入
[1,2,3,4,5],输出3(路径4→2→5或4→2→1→3,边数为 3)。
2. 核心思想(树形 DP)
同样是后序遍历,核心逻辑:
- 子树信息:左 / 右子树的深度(深度 = 子树的节点数);
- 当前节点最优:以当前节点为核心的路径边数(左深度 + 右深度),用它更新全局最大直径;
- 向上返回信息:当前节点的深度(max (左深度,右深度) + 1)------ 父节点的深度依赖当前节点的深度。
3. 解法实现
(1)基础递归解法(时间 O (n),空间 O (h))
java
class Solution {
// 全局变量存最大直径(边数,初始为0)
private int maxDiameter = 0;
public int diameterOfBinaryTree(TreeNode root) {
calcDepth(root);
return maxDiameter;
}
// 递归函数:返回当前节点的深度(节点数)
private int calcDepth(TreeNode node) {
if (node == null) return 0; // 空节点深度为0
int leftDepth = calcDepth(node.left);
int rightDepth = calcDepth(node.right);
// 计算当前节点的路径边数,更新全局直径
int currentDiameter = leftDepth + rightDepth;
maxDiameter = Math.max(maxDiameter, currentDiameter);
// 向父节点返回当前节点的深度
return Math.max(leftDepth, rightDepth) + 1;
}
}
(2)优化:迭代后序遍历
java
class Solution {
public int diameterOfBinaryTree(TreeNode root) {
if (root == null) return 0;
int maxDiameter = 0;
Deque<TreeNode> stack = new LinkedList<>();
// 存储每个节点的深度
Map<TreeNode, Integer> depthMap = new HashMap<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.peek();
if (node.left != null && !depthMap.containsKey(node.left)) {
stack.push(node.left);
} else if (node.right != null && !depthMap.containsKey(node.right)) {
stack.push(node.right);
} else {
stack.pop();
int left = node.left == null ? 0 : depthMap.get(node.left);
int right = node.right == null ? 0 : depthMap.get(node.right);
// 更新全局直径
maxDiameter = Math.max(maxDiameter, left + right);
// 存储当前节点的深度
depthMap.put(node, Math.max(left, right) + 1);
}
}
return maxDiameter;
}
}
4. 易错点
- 混淆 "节点数" 和 "边数":直径是边数,等于 "左深度 + 右深度"(深度是节点数),不要多 + 1;
- 全局变量初始化为 1:边数的最小值是 0(单个节点的情况),需初始化为 0。
三、树形 DP 常见题型的核心模板
树形 DP 的核心是 **"后序遍历 + 子树信息传递 + 全局最优更新"**,以下是通用模板:
1. 模板结构
- 定义全局变量:存储题目要求的 "全局最优解"(如最大路径和、最大直径);
- 递归函数(后序遍历) :
- 处理空节点的边界条件;
- 递归遍历左、右子树,获取子树的 "局部信息";
- 计算 "以当前节点为核心的局部最优解",更新全局变量;
- 返回 "父节点需要的信息"(子树传递给父节点的信息)。
2. 通用代码框架
java
class Solution {
// 1. 定义全局变量存全局最优解
private [类型] globalResult = [初始值];
public [类型] solve(TreeNode root) {
// 2. 调用后序遍历的递归函数
postOrder(root);
return globalResult;
}
// 递归函数:返回父节点需要的信息
private [子树信息类型] postOrder(TreeNode node) {
// 3. 空节点的边界条件
if (node == null) return [空节点的信息值];
// 4. 递归处理左、右子树,获取子树信息
[子树信息类型] leftInfo = postOrder(node.left);
[子树信息类型] rightInfo = postOrder(node.right);
// 5. 计算当前节点的局部最优解,更新全局结果
[局部最优解] currentOpt = 计算逻辑(leftInfo, rightInfo, node);
globalResult = Math.max(globalResult, currentOpt);
// 6. 返回父节点需要的信息
return 父节点需要的信息逻辑(leftInfo, rightInfo, node);
}
}
3. 题型适配示例
将模板对应到前两道题:
| 题型 | 全局变量类型 / 初始值 | 子树信息类型 / 空节点值 | 局部最优解计算逻辑 | 父节点需要的信息逻辑 |
|---|---|---|---|---|
| 最大路径和 | int/Integer.MIN_VALUE | int/0 | leftInfo + rightInfo + node.val | node.val + Math.max(leftInfo, rightInfo) |
| 二叉树的直径 | int/0 | int/0 | leftInfo + rightInfo | Math.max(leftInfo, rightInfo) + 1 |
4. 适用场景
该模板适用于所有 "树的路径 / 子树相关最优解" 问题,比如:
- 最长同值路径(LeetCode 687);
- 二叉树中的最长交错路径(LeetCode 1372);
- 打家劫舍 III(LeetCode 337)。
