Sum of Distances in Tree
HardPrompt
There is an undirected connected tree with n nodes labeled from 0 to n - 1 and n - 1 edges.
You are given the integer n and the array edges where edges[i] = [ai, bi] indicates that there is an edge between nodes ai and bi in the tree.
Return an array answer of length n where answer[i] is the sum of the distances between the ith node in the tree and all other nodes.
Example 1:
Input: n = 6, edges = [[0,1],[0,2],[2,3],[2,4],[2,5]]
Output: [8,12,6,10,10,10]
Explanation: The tree is shown above.
We can see that dist(0,1) + dist(0,2) + dist(0,3) + dist(0,4) + dist(0,5)
equals 1 + 1 + 2 + 2 + 2 = 8.
Hence, answer[0] = 8, and so on.Example 2:
Input: n = 1, edges = []
Output: [0]Example 3:
Input: n = 2, edges = [[1,0]]
Output: [1,1]
Constraints:
1 <= n <= 3 * 104edges.length == n - 1edges[i].length == 20 <= ai, bi < nai != bi- The given input represents a valid tree.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach is the most straightforward. For every single node in the tree, we can calculate the sum of its distances to all other nodes. To find the distance from a starting node u to all other nodes v, we can use a graph traversal algorithm like Breadth-First Search (BFS) or Depth-First Search (DFS). BFS is a natural choice here as it explores the tree layer by layer, directly giving the shortest distance from the source.
Algorithm
-
- Create an adjacency list
adjto represent the tree.
- Create an adjacency list
-
- Initialize an
answerarray of sizen.
- Initialize an
-
- For each node
ifrom0ton-1:
- a. Initialize a queue for BFS and add
ito it. - b. Initialize a
distancesarray of sizen(e.g., filled with -1) and setdistances[i] = 0. - c. Initialize a variable
currentSum = 0. - d. While the queue is not empty:
- i. Dequeue a node
u. - ii. Add
distances[u]tocurrentSum. - iii. For each neighbor
vofu:- If
vhas not been visited (distances[v] == -1):- Set
distances[v] = distances[u] + 1. - Enqueue
v.
- Set
- If
- i. Dequeue a node
- e. Store the result:
answer[i] = currentSum.
- For each node
-
- Return the
answerarray.
- Return the
Walkthrough
We iterate through each node i from 0 to n-1. For each i, we perform a BFS starting from it to compute the distances to all other nodes. A queue is used for the BFS, storing pairs of (node, distance). We also need a visited array for each BFS run to avoid re-visiting nodes. During the BFS starting from i, we maintain a running sum. When we visit a node j at a distance d, we add d to this sum. After the BFS completes, the total sum is the answer for node i, so we store it in answer[i]. This process is repeated for all n nodes. The first step is to build an adjacency list representation of the tree from the input edges array to facilitate the traversal.
import java.util.*; class Solution { public int[] sumOfDistancesInTree(int n, int[][] edges) { if (n == 1) { return new int[]{0}; } List<List<Integer>> adj = new ArrayList<>(); for (int i = 0; i < n; i++) { adj.add(new ArrayList<>()); } for (int[] edge : edges) { adj.get(edge[0]).add(edge[1]); adj.get(edge[1]).add(edge[0]); } int[] answer = new int[n]; for (int i = 0; i < n; i++) { answer[i] = bfs(i, n, adj); } return answer; } private int bfs(int startNode, int n, List<List<Integer>> adj) { int[] dist = new int[n]; Arrays.fill(dist, -1); Queue<Integer> queue = new LinkedList<>(); queue.offer(startNode); dist[startNode] = 0; int totalDistance = 0; while (!queue.isEmpty()) { int u = queue.poll(); totalDistance += dist[u]; for (int v : adj.get(u)) { if (dist[v] == -1) { dist[v] = dist[u] + 1; queue.offer(v); } } } return totalDistance; }}Complexity
Time
O(N^2). For each of the N nodes, we perform a BFS traversal. A single BFS on a tree takes O(N + E) time, where E is the number of edges (N-1). So, one BFS is O(N). Repeating this for all N nodes results in a total time complexity of O(N * N) = O(N^2).
Space
O(N). The adjacency list requires O(N + E) = O(N) space. The queue and the `distances` array used in each BFS also require O(N) space.
Trade-offs
Pros
Conceptually simple and easy to implement.
It's a direct translation of the problem statement.
Cons
Inefficient. The O(N^2) time complexity will result in a "Time Limit Exceeded" (TLE) for larger inputs (like N = 3 * 10^4).
It performs a lot of redundant calculations. The distance between two nodes
uandvis calculated twice, once when starting fromuand once when starting fromv.
A more efficient solution can be achieved by recognizing the relationship between the answers for adjacent nodes. Instead of re-calculating everything from scratch for each node, we can derive the answer for a node based on the answer of its parent. This dynamic programming approach on the tree requires two Depth-First Search (DFS) traversals.
Algorithm
-
- Build an adjacency list
adjfor the tree.
- Build an adjacency list
-
- Initialize two arrays,
count(for subtree sizes) andans(for the final answer), both of sizen.
- Initialize two arrays,
-
- First Pass (post-order DFS, e.g.,
dfs1(u, parent)):
- a. Start DFS from an arbitrary root (e.g., node 0).
- b. For a node
u, recursively calldfs1for all its childrenv. - c. After the children's calls return, update
count[u]andans[u]:count[u] = 1 + Σ count[v]for all childrenv.ans[u] = Σ (ans[v] + count[v])for all childrenv.
- First Pass (post-order DFS, e.g.,
-
- Second Pass (pre-order DFS, e.g.,
dfs2(u, parent)):
- a. Start DFS from the same root (node 0).
- b. For a node
u, before visiting its children, itsans[u]is already correct. - c. For each child
vofu, calculate its final answer using the parent's answer:ans[v] = ans[u] - count[v] + (n - count[v]).
- d. Recursively call
dfs2for the childv.
- Second Pass (pre-order DFS, e.g.,
-
- Return the
ansarray.
- Return the
Walkthrough
The core idea is to first calculate the answer for an arbitrary root (say, node 0) and then use this result to efficiently calculate the answers for all other nodes. This is done in two passes.
Pass 1: Post-order Traversal (Bottom-up)
We perform a DFS starting from root 0. This traversal will compute two things for each node u:
count[u]: The number of nodes in the subtree rooted atu(includingu).ans[u]: The sum of distances fromuto all other nodes within its own subtree. The DFS explores children first. After returning from all childrenvofu, we can computecount[u]andans[u]for the parentu.count[u] = 1 + sum(count[v])for all childrenv.ans[u] = sum(ans[v] + count[v])for all childrenv. The termans[v]is the sum of distances fromvto its descendants. To get the sum of distances fromutov's descendants, we note that each path is one edge longer (the edgeu-v), so we addcount[v](the number of such descendants). After this pass,ans[0]holds the correct final answer for the root node, as its subtree is the entire tree.
Pass 2: Pre-order Traversal (Top-down)
We perform a second DFS, also starting from root 0. This traversal will use the parent's correct answer to calculate the child's correct answer. When we move from a parent u to a child v, the sum of distances changes.
- For the
count[v]nodes inv's subtree, their distance to the new "root"vbecomes 1 shorter. This contributes a change of-count[v]. - For the
n - count[v]nodes outsidev's subtree, their distance to the new "root"vbecomes 1 longer. This contributes a change of+(n - count[v]). So, the update formula is:ans[v] = ans[u] - count[v] + (n - count[v]). We apply this formula as we traverse down the tree, and after this pass, theansarray is fully populated with the correct values.
import java.util.*; class Solution { private List<List<Integer>> adj; private int[] count; private int[] ans; private int n; public int[] sumOfDistancesInTree(int n, int[][] edges) { this.n = n; this.adj = new ArrayList<>(); for (int i = 0; i < n; i++) { adj.add(new ArrayList<>()); } for (int[] edge : edges) { adj.get(edge[0]).add(edge[1]); adj.get(edge[1]).add(edge[0]); } this.count = new int[n]; this.ans = new int[n]; // Pass 1: Post-order traversal to calculate subtree sizes and partial sums dfs1(0, -1); // Pass 2: Pre-order traversal to update sums for all nodes dfs2(0, -1); return ans; } // Post-order traversal: calculate count[u] and ans[u] for the subtree rooted at u private void dfs1(int u, int parent) { count[u] = 1; for (int v : adj.get(u)) { if (v == parent) continue; dfs1(v, u); count[u] += count[v]; ans[u] += ans[v] + count[v]; } } // Pre-order traversal: calculate the final answer for each node private void dfs2(int u, int parent) { for (int v : adj.get(u)) { if (v == parent) continue; // ans[v] is currently the sum of distances in its subtree. // We need to update it based on the parent's final answer. // ans[u] is the final answer for node u. // Move root from u to v: // - nodes in v's subtree get 1 closer: -count[v] // - nodes outside v's subtree get 1 farther: +(n - count[v]) ans[v] = ans[u] - count[v] + (n - count[v]); dfs2(v, u); } }}Complexity
Time
O(N). We perform two separate DFS traversals on the tree. Each traversal visits every node and edge exactly once. Building the adjacency list also takes O(N) time. Thus, the total time complexity is linear with respect to the number of nodes.
Space
O(N). The adjacency list requires O(N) space. The `count` and `ans` arrays also take O(N) space. The recursion stack for DFS can be up to O(N) in the worst case (for a skewed tree), leading to an overall O(N) space complexity.
Trade-offs
Pros
Highly efficient with linear time complexity, making it suitable for large inputs.
It's the optimal way to solve this problem.
Cons
The logic is more complex than the brute-force approach.
It requires a good understanding of tree traversals and dynamic programming on trees.
Solutions
Solution
class Solution {private int n;private int[] ans;private int[] size;private List<Integer>[] g;public int[] sumOfDistancesInTree(int n, int[][] edges) { this.n = n; g = new List[n]; ans = new int[n]; size = new int[n]; Arrays.setAll(g, k->new ArrayList<>()); for (var e : edges) { int a = e[0], b = e[1]; g[a].add(b); g[b].add(a); } dfs1(0, -1, 0); dfs2(0, -1, ans[0]); return ans; }private void dfs1(int i, int fa, int d) { ans[0] += d; size[i] = 1; for (int j : g[i]) { if (j != fa) { dfs1(j, i, d + 1); size[i] += size[j]; } } }private void dfs2(int i, int fa, int t) { ans[i] = t; for (int j : g[i]) { if (j != fa) { dfs2(j, i, t - size[j] + n - size[j]); } } }}class Solution {public: vector<int> sumOfDistancesInTree(int n, vector<vector<int>> &edges) { vector<vector<int>> g(n); for (auto &e : edges) { int a = e[0], b = e[1]; g[a].push_back(b); g[b].push_back(a); } vector<int> ans(n); vector<int> size(n); function<void(int, int, int)> dfs1 = [&](int i, int fa, int d) { ans[0] += d; size[i] = 1; for (int &j : g[i]) { if (j != fa) { dfs1(j, i, d + 1); size[i] += size[j]; } } }; function<void(int, int, int)> dfs2 = [&](int i, int fa, int t) { ans[i] = t; for (int &j : g[i]) { if (j != fa) { dfs2(j, i, t - size[j] + n - size[j]); } } }; dfs1(0, -1, 0); dfs2(0, -1, ans[0]); return ans; }};class Solution: def sumOfDistancesInTree(self, n: int, edges: List[List[int]]) -> List[int]: def dfs1(i: int, fa: int, d: int): ans[0] += d size[i] = 1 for j in g[i]: if j != fa: dfs1(j, i, d + 1) size[i] += size[j] def dfs2(i: int, fa: int, t: int): ans[i] = t for j in g[i]: if j != fa: dfs2(j, i, t - size[j] + n - size[j]) g = defaultdict(list) for a, b in edges: g[a]. append(b) g[b]. append(a) ans = [0] * n size = [0] * n dfs1(0, - 1, 0) dfs2(0, - 1, ans[0]) return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.