LeetCode 834. 树中距离之和
题目描述



题意分析
对树中的每个节点,求它到所有其他节点的边数之和。若从每个节点分别遍历一次,会重复计算大量相同路径;相邻节点的距离和只差一次跨边后的增减,可以先算一个节点,再递推出其余答案。
解法:树形 DP + 换根
核心思路
[!blue]
先以 0 为根,记录
parent和父节点先于孩子的顺序order。树中没有环,遍历邻接节点时排除父节点就不会重复访问。将size[u]初始化为 1,表示当前只统计节点自身;answer[u]初始为 0,第一遍用于记录u到其原根子树中所有节点的距离和。逆序处理
order时,孩子都先于父节点完成统计。设u的父节点为p,将size[u]加到size[p],并将answer[u] + size[u]加到answer[p]:从p到u子树中的每个节点,都要先多走p-u这一条边。汇总结束后,根的子树就是整棵树,所以answer[0]已是全局距离和。再按正序把全局答案从父节点传给孩子。沿
p-u从p移到u后,u原子树中的size[u]个节点都近了 1,其余n - size[u]个节点都远了 1,因为树中两点之间只有一条路径。因此answer[u] = answer[p] - size[u] + (n - size[u]),也就是answer[p] + n - 2 * size[u]。正序保证使用父节点时,它的
answer已经是全树答案,随后覆盖孩子之前的局部距离和。size始终保留以 0 为根时的定义:它表示删去父子边后孩子一侧的节点数,不需要真的修改树结构或重新计算子树。
解题步骤
- 建立无向邻接表,从 0 出发生成
parent和order,将所有size设为 1、answer设为 0。- 从
order末尾处理到下标 1,把当前子树的大小和距离贡献累加给父节点;根没有父节点,必须跳过。- 从下标 1 正向处理,使用
answer[parent[u]] + n - 2 * size[u]得到每个节点的全局答案。- 返回
answer。只有一个节点时,两遍循环都跳过,结果自然为 0。
代码实现
class Solution {
public int[] sumOfDistancesInTree(int n, int[][] edges) {
List<Integer>[] graph = new ArrayList[n];
for (int i = 0; i < n; i++) {
graph[i] = new ArrayList<>();
}
for (int[] edge : edges) {
graph[edge[0]].add(edge[1]);
graph[edge[1]].add(edge[0]);
}
int[] parent = new int[n];
// 零号为初始根,遍历时排除返回父节点的边
parent[0] = -1;
int[] order = new int[n];
int count = 1;
for (int i = 0; i < count; i++) {
int u = order[i];
for (int v : graph[u]) {
if (v == parent[u]) {
continue;
}
parent[v] = u;
// 先记录父节点再记录孩子,后续可按相反方向汇总
order[count++] = v;
}
}
int[] size = new int[n];
Arrays.fill(size, 1);
int[] answer = new int[n];
// 逆序保证孩子已经完成子树统计,根不再向上汇总
for (int i = n - 1; i > 0; i--) {
int u = order[i];
int p = parent[u];
size[p] += size[u];
// 子树每个节点到父节点的距离都比到孩子多一
answer[p] += answer[u] + size[u];
}
for (int i = 1; i < n; i++) {
int u = order[i];
// 换根后子树内距离减一,外部距离加一;大小仍按原根定义
answer[u] = answer[parent[u]] + n - 2 * size[u];
}
return answer;
}
}
func sumOfDistancesInTree(n int, edges [][]int) []int {
graph := make([][]int, n)
for _, edge := range edges {
graph[edge[0]] = append(graph[edge[0]], edge[1])
graph[edge[1]] = append(graph[edge[1]], edge[0])
}
parent := make([]int, n)
// 零号为初始根,遍历时排除返回父节点的边
parent[0] = -1
order := make([]int, 1, n)
for i := 0; i < len(order); i++ {
u := order[i]
for _, v := range graph[u] {
if v == parent[u] {
continue
}
parent[v] = u
// 先记录父节点再记录孩子,后续可按相反方向汇总
order = append(order, v)
}
}
size := make([]int, n)
for i := range size {
size[i] = 1
}
answer := make([]int, n)
// 逆序保证孩子已经完成子树统计,根不再向上汇总
for i := n - 1; i > 0; i-- {
u := order[i]
p := parent[u]
size[p] += size[u]
// 子树每个节点到父节点的距离都比到孩子多一
answer[p] += answer[u] + size[u]
}
for i := 1; i < n; i++ {
u := order[i]
// 换根后子树内距离减一,外部距离加一;大小仍按原根定义
answer[u] = answer[parent[u]] + n - 2*size[u]
}
return answer
}
复杂度分析
- 时间复杂度:$O(n)$,建图与两遍遍历线性。
- 空间复杂度:$O(n)$,邻接表、父数组、遍历顺序、大小和答案数组,不依赖递归深度。
关键点总结
[!green]
- 逆序用于子树汇总,正序用于把父节点的全局答案传向孩子。
- 换根只更新距离和,不真的改动树的连接。
- 子树大小仍以最初的零号根为准。
易错点总结
[!yellow]
- 汇总时漏掉 size[child],会忽略父节点到孩子这一条边给每个子树节点增加的距离。
- 没有先算出父节点的全局答案就换根,会读取局部距离和。
- 换根时重算或修改子树大小,会破坏递推式的原始划分。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 2581. 统计可能的树根数目 | 困难 | 同样先按一个根计算,再沿树边换根修正答案,原题调整正确猜测数,本题调整全部节点距离和。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!