leetcode
leetcode 751 ~ 800
树中距离之和

树中距离之和

难度:

标签:

题目描述

给定一个无向、连通的树。树中有 n 个标记为 0...n-1 的节点以及 n-1 条边 。

给定整数 n 和数组 edges , edges[i] = [ai, bi]表示树中的节点 ai 和 bi 之间有一条边。

返回长度为 n 的数组 answer ,其中 answer[i] 是树中第 i 个节点与所有其他节点之间的距离之和。

 

示例 1:

输入: n = 6, edges = [[0,1],[0,2],[2,3],[2,4],[2,5]]
输出: [8,12,6,10,10,10]
解释: 树如图所示。
我们可以计算出 dist(0,1) + dist(0,2) + dist(0,3) + dist(0,4) + dist(0,5) 
也就是 1 + 1 + 2 + 2 + 2 = 8。 因此,answer[0] = 8,以此类推。

示例 2:

输入: n = 1, edges = []
输出: [0]

示例 3:

输入: n = 2, edges = [[1,0]]
输出: [1,1]

 

提示:

  • 1 <= n <= 3 * 104
  • edges.length == n - 1
  • edges[i].length == 2
  • 0 <= ai, bi < n
  • ai != bi
  • 给定的输入保证为有效的树

代码结果

运行时间: 175 ms, 内存: 40.3 MB


/*
 * 思路:
 * 1. 使用流构建树的邻接表表示。
 * 2. 使用两次深度优先搜索(DFS)来计算结果,配合流处理数据。
 */
import java.util.*;
import java.util.stream.*;

public class Solution {
    int[] answer, count;
    List<Set<Integer>> tree;
    int n;
    
    public int[] sumOfDistancesInTree(int N, int[][] edges) {
        n = N;
        tree = IntStream.range(0, n)
                         .mapToObj(i -> new HashSet<Integer>())
                         .collect(Collectors.toList());
        Arrays.stream(edges).forEach(e -> {
            tree.get(e[0]).add(e[1]);
            tree.get(e[1]).add(e[0]);
        });
        answer = new int[n];
        count = new int[n];
        Arrays.fill(count, 1);
        dfs(0, -1);
        dfs2(0, -1);
        return answer;
    }

    public void dfs(int node, int parent) {
        tree.get(node).stream()
            .filter(neighbor -> neighbor != parent)
            .forEach(neighbor -> {
                dfs(neighbor, node);
                count[node] += count[neighbor];
                answer[node] += answer[neighbor] + count[neighbor];
            });
    }

    public void dfs2(int node, int parent) {
        tree.get(node).stream()
            .filter(neighbor -> neighbor != parent)
            .forEach(neighbor -> {
                answer[neighbor] = answer[node] - count[neighbor] + n - count[neighbor];
                dfs2(neighbor, node);
            });
    }
}

解释

方法:

该题解采用两遍DFS(深度优先搜索)的方法。首先,建立邻接表表示树。第一次DFS计算出根节点(假设为节点0)到所有其他节点的距离总和并存储在ans[0]中,同时计算每个节点为根的子树的大小存储在size数组中。第二次DFS则利用已知的根节点的答案和子树大小,通过换根DP(动态规划)的思想,推导出其他节点的答案。具体地,当从节点x到其子节点y时,ans[y] = ans[x] + n - 2 * size[y],这是基于树中每当根节点改变时,到达某个节点的距离改变的性质计算的。这种方法有效地避免了重复计算每个节点到所有其他节点的距离,大大提高了效率。

时间复杂度:

O(n)

空间复杂度:

O(n)

代码细节讲解

🦆
为什么选择节点0作为初始的根节点进行第一次DFS,是否选择其他节点作为根节点会影响最终的结果?
选择节点0作为初始的根节点主要是出于方便和约定的原因,因为在大多数编程问题中,数组或列表的索引通常从0开始。实际上,选择任何一个节点作为根节点进行算法的第一次DFS都是可以的,这不会影响最终结果的正确性。算法的核心在于正确计算每个节点作为根时的子树大小和距离和。由于树是一个无环连通图,因此从任何一个节点开始,都能遍历整棵树,并得出正确的子树大小和初次计算的距离和。
🦆
在第一次DFS过程中,`size`数组的具体作用是什么?如何理解每个节点的`子树大小`对于计算距离和的影响?
`size`数组用于存储每个节点的子树中节点的总数(包括该节点本身)。在第一次DFS中,每遍历到一个节点,就更新其父节点的`size`值,增加当前节点子树的大小。这个信息对于第二次DFS中距离的快速计算至关重要。了解每个节点的子树大小可以帮助我们快速计算当根节点从一个节点变更到其子节点时,其他所有节点到新根节点的距离之和。具体来说,当根节点变更时,原根节点子树中的节点到新根的距离都会增加,而新根节点子树外的节点到新根的距离则会减少,这种变化可以通过子树大小直接计算得出,而不需要逐个遍历。
🦆
第二次DFS中,公式`ans[y] = ans[x] + n - 2 * size[y]`是如何得出的?可以详细解释这个公式背后的数学逻辑吗?
这个公式的推导基于树的性质,当根节点从x变更到其子节点y时,对于y的子树中的所有节点,它们到y的距离相比到x的距离减少了1。而对于不在y的子树中的节点,包括x,它们到y的距离相比到x的距离增加了1。因此,可以将这种变化量表示为:`总距离的变化量 = -1 * (y子树中的节点数) + 1 * (不在y子树中的节点数)`。由于整棵树有n个节点,不在y子树中的节点数即为`n - size[y]`。因此,`总距离的变化量`可以进一步写成 `n - 2 * size[y]`。将这个变化量加到原根节点x的距离和上,即得到新根节点y的距离和:`ans[y] = ans[x] + n - 2 * size[y]`。

相关问题

在二叉树中分配硬币

给你一个有 n 个结点的二叉树的根结点 root ,其中树中每个结点 node 都对应有 node.val 枚硬币。整棵树上一共有 n 枚硬币。

在一次移动中,我们可以选择两个相邻的结点,然后将一枚硬币从其中一个结点移动到另一个结点。移动可以是从父结点到子结点,或者从子结点移动到父结点。

返回使每个结点上 只有 一枚硬币所需的 最少 移动次数。

 

示例 1:

输入:root = [3,0,0]
输出:2
解释:一枚硬币从根结点移动到左子结点,一枚硬币从根结点移动到右子结点。

示例 2:

输入:root = [0,3,0]
输出:3
解释:将两枚硬币从根结点的左子结点移动到根结点(两次移动)。然后,将一枚硬币从根结点移动到右子结点。

 

提示:

  • 树中节点的数目为 n
  • 1 <= n <= 100
  • 0 <= Node.val <= n
  • 所有 Node.val 的值之和是 n