这是一个经典的二叉树预处理问题。为了高效回答多个查询我们需要在()O(N) 或(log⁡)O(NlogN) 的时间内预处理出每个节点被移除后剩余树的高度从而使得每次查询可以在(1)O(1) 时间内完成。核心思路如果直接对每个查询执行一次 DFS 来计算高度时间复杂度将是(⋅)O(M⋅N)其中M 是查询次数N 是节点数。由于N 可达105105这会超时。优化策略‌计算子树高度‌首先通过一次 DFS 计算每个节点作为根的子树的高度记为 subtreeHeight[node]。计算移除后的高度‌我们需要知道如果移除了节点 u 的子树剩下的树的高度是多少。树的高度由从根到叶子的最长路径决定。如果移除节点 u那么所有经过 u 的路径都断了。剩余树的高度取决于‌不经过 u 的最深路径‌。对于节点 u 的父节点 p如果 u 是 p 的左孩子那么移除 u 后p 这一侧贡献的高度就变成了 p 的右子树高度 1如果右子树存在或者仅仅是 p 的深度如果右子树为空。更准确地说我们需要维护每个节点在其祖先路径上的“兄弟方向”的最大深度信息。更高效的预处理方法两次 DFS‌我们可以定义 maxHeightWithoutSubtree[node] 表示移除以 node 为根的子树后整棵树的高度。第一次 DFS (Post-order)‌: 计算每个节点的子树高度 h[node]。h[node] 1 max(h[left], h[right])叶子节点高度为 0根据题目定义高度是边数所以叶子节点高度为0空节点高度为-1或者单独处理。通常定义空节点高度-1叶子节点高度0。或者定义节点数为高度最后减1。这里采用边数定义叶子节点高度0。第二次 DFS (Pre-order)‌: 计算移除每个节点子树后的剩余树高度。设当前节点为 curr其深度为 depth。对于 curr 的左孩子 left如果移除 left 的子树那么从 curr 往下的最大深度将不再包含 left 分支。此时经过 curr 的其他路径的最大深度是 depth 1 h[right] (如果右孩子存在)。但是剩余树的全局高度可能来自 curr 的祖先的其他分支。我们需要传递一个值 maxDepthFromAncestors表示在不经过 curr 及其后代的情况下从根到某叶子的最大深度。更简洁的视角‌对于任意节点 u移除其子树后树的高度等于max( 所有不经过 u 的从根到叶子的路径长度 )我们可以预处理两个数组prefixMax[i]: 在 DFS 序中前 i 个节点对应的子树深度的最大值这里的深度指该子树能提供的最大路径长度即 depth[node] subtreeHeight[node]。suffixMax[i]: 在 DFS 序中后 i 个节点对应的子树深度的最大值。这种方法需要用到 DFS 序和区间最大值的概念稍微复杂。推荐解法维护“来自祖先的最大替代高度”‌我们定义 ans[node] 为移除 node 子树后树的高度。在进行第二次 DFS 时我们携带一个参数 maxHeightExcludingCurrentSubtree它表示‌如果在当前节点的父节点处不走当前节点这条分支能达到的最大全局高度。‌具体步骤计算每个节点的 subtreeHeight。进行第二次 DFS传入当前节点 node当前深度 depth以及 maxHeightFromOtherBranches。maxHeightFromOtherBranches 的含义是在不经过 node 的任何后代的情况下树中能达到的最大高度。这个值其实就是移除 node 子树后的答案 ans[node] 的候选值之一但实际上移除 node 后树的高度就是 maxHeightFromOtherBranches 吗不完全是。maxHeightFromOtherBranches 应该是在 node 的父节点层面计算的“其他分支的最大深度”。让我们重新梳理第二次 DFS 的逻辑函数 dfs2(node, depth, maxH)maxH: 表示如果移除 node 所在的整个子树剩余部分能提供的最大高度。也就是 ans[node] maxH。对于 node 的孩子 child如果我们要计算移除 child 子树后的高度我们需要知道除了 child 分支外其他所有路径的最大高度。其他路径包括node 的其他子节点分支兄弟分支。node 的祖先的其他分支即传入的 maxH 的来源。具体来说对于 node 的左孩子 L 和右孩子 R移除 L 后经过 node 的最大高度变为 depth 1 subtreeHeight[R] (如果 R 存在)。全局最大高度则是 max( maxH, depth 1 subtreeHeight[R] )。所以递归调用 dfs2(L, depth 1, max( maxH, depth 1 (R ? subtreeHeight[R] : -1) ) )。同理dfs2(R, depth 1, max( maxH, depth 1 (L ? subtreeHeight[L] : -1) ) )。初始调用dfs2(root, 0, 0)?根节点不能被移除题目保证 queries[i] ! root.val但我们需要初始化。对于根的孩子maxH 应该是 0 吗如果移除根的左孩子剩余高度至少是根的右子树高度1。如果没有右子树高度可能是0如果根也是叶子但根有左孩子所以根不是叶子。注意题目定义高度为边数。如果树只有根和左孩子。移除左孩子剩根高度0。subtreeHeight[leaf] 0.dfs2(root, 0, 0).左孩子 L: maxH_new max(0, 0 1 subtreeHeight[R]). 如果 R 为空subtreeHeight 设为 -1。则 max(0, 0) 0. ans[L] 0. 正确。Java 代码实现javaimport java.util.HashMap;import java.util.Map;// Definition for a binary tree node.class TreeNode {int val;TreeNode left;TreeNode right;TreeNode() {}TreeNode(int val) { this.val val; }TreeNode(int val, TreeNode left, TreeNode right) {this.val val;this.left left;this.right right;}}class Solution {// 存储每个节点的子树高度private MapInteger, Integer subtreeHeightMap new HashMap();// 存储移除每个节点子树后树的高度private MapInteger, Integer answerMap new HashMap();public int[] treeQueries(TreeNode root, int[] queries) { // 1. 计算每个节点的子树高度 calculateSubtreeHeight(root); // 2. 计算移除每个节点后的高度 // maxH 表示不经过当前节点子树的情况下其他部分能提供的最大高度 // 对于根节点虽然没有“其他部分”但我们从根的孩子开始计算 // 根节点本身不会被查询所以不需要计算 ans[root] // 初始 maxH 为 0。 // 为什么是0因为如果移除根的一个孩子且根没有另一个孩子 // 剩下的树只有根节点高度为0。 dfs2(root, 0, 0); // 3. 构建结果数组 int[] result new int[queries.length]; for (int i 0; i queries.length; i) { result[i] answerMap.get(queries[i]); } return result; } // 第一次 DFS计算子树高度 private int calculateSubtreeHeight(TreeNode node) { if (node null) { return -1; // 空节点高度定义为 -1这样叶子节点高度为 0 (1 max(-1, -1) 0? No. 1(-1)0 is correct for edge count if we do 1child) // 等等通常高度定义 // 叶子节点高度 0。 // null 节点高度 -1。 // node.height 1 max(left.height, right.height) // 如果 leftnull, rightnull: 1 max(-1, -1) 0. 正确。 } int leftH calculateSubtreeHeight(node.left); int rightH calculateSubtreeHeight(node.right); int h 1 Math.max(leftH, rightH); subtreeHeightMap.put(node.val, h); return h; } // 第二次 DFS计算移除子树后的高度 // node: 当前节点 // depth: 当前节点的深度 (根节点深度为 0) // maxH: 移除 node 子树后剩余树的高度。 // 更准确地说这是从祖先传下来的“不包含 node 及其兄弟子树”的最大高度路径。 // 结合兄弟子树的高度就能算出移除 node 的孩子时的答案。 private void dfs2(TreeNode node, int depth, int maxH) { if (node null) return; // 记录移除当前节点子树后的高度 // 对于根节点这个调用可能不会发生或者 maxH 无意义但题目说 queries[i] ! root.val answerMap.put(node.val, maxH); int leftH node.left ! null ? subtreeHeightMap.get(node.left.val) : -1; int rightH node.right ! null ? subtreeHeightMap.get(node.right.val) : -1; // 如果去左孩子剩下的最大高度是 // max( 祖先传来的最大高度 maxH, 当前节点深度 1 右子树高度 ) // 注意depth 是当前节点 node 的深度。 // 从根到 node 的孩子比如左孩子的路径长度是 depth 1。 // 如果走右子树最大深度是 (depth 1) rightH。 // 但是 maxH 已经是全局的最大高度了基于边的数量。 // 我们需要比较的是“路径长度”。 // 让我们统一单位都是“从根到叶子的边数”。 // maxH 是一个完整的树高度值。 // depth 1 rightH 也是一个完整的路径长度值从根经过 node 到右子树最深叶子。 if (node.left ! null) { // 移除左子树后经过 node 的最佳路径是经过右子树 int heightViaRight depth 1 rightH; // 全局最佳是 祖先其他分支(maxH) 和 当前兄弟分支(heightViaRight) 的最大值 int newMaxHForLeftChild Math.max(maxH, heightViaRight); dfs2(node.left, depth 1, newMaxHForLeftChild); } if (node.right ! null) { // 移除右子树后经过 node 的最佳路径是经过左子树 int heightViaLeft depth 1 leftH; int newMaxHForRightChild Math.max(maxH, heightViaLeft); dfs2(node.right, depth 1, newMaxHForRightChild); } }}复杂度分析时间复杂度‌:calculateSubtreeHeight: 遍历所有节点一次()O(N)。dfs2: 遍历所有节点一次()O(N)。处理查询()O(M)。总时间复杂度:()O(NM)。空间复杂度‌:哈希表存储高度和答案()O(N)。递归栈深度()O(H)最坏情况()O(N)。总空间复杂度:()O(N)。关键点解释subtreeHeight 的定义‌这里定义为从该节点到其最深叶子节点的‌边数‌。空节点返回 -1叶子节点返回 0。maxH 的传递‌当我们在节点 node 时maxH 代表了“如果不看 node 这棵子树包括 node 的兄弟子树整棵树其他地方能达到的最大高度”。当我们要进入 node 的左孩子 left 时我们需要计算“如果移除 left 的子树整棵树的最大高度”。这个高度由两部分竞争产生完全不经过 node 这一支的路径即 maxH。经过 node 但不经过 left 的路径。这条路径必然经过 node 的右孩子如果存在。其长度为 depth(node) 1 subtreeHeight(right)。取这两者的最大值即为传递给 left 的新的 maxH。这种方法的巧妙之处在于它在自上而下的遍历中动态地维护了“排除当前子树后的全局最优解”避免了重复计算。