Count Nodes With the Highest Score
MedPrompt
There is a binary tree rooted at 0 consisting of n nodes. The nodes are labeled from 0 to n - 1. You are given a 0-indexed integer array parents representing the tree, where parents[i] is the parent of node i. Since node 0 is the root, parents[0] == -1.
Each node has a score. To find the score of a node, consider if the node and the edges connected to it were removed. The tree would become one or more non-empty subtrees. The size of a subtree is the number of the nodes in it. The score of the node is the product of the sizes of all those subtrees.
Return the number of nodes that have the highest score.
Example 1:
Input: parents = [-1,2,0,2,0]
Output: 3
Explanation:
- The score of node 0 is: 3 * 1 = 3
- The score of node 1 is: 4 = 4
- The score of node 2 is: 1 * 1 * 2 = 2
- The score of node 3 is: 4 = 4
- The score of node 4 is: 4 = 4
The highest score is 4, and three nodes (node 1, node 3, and node 4) have the highest score.Example 2:
Input: parents = [-1,2,0]
Output: 2
Explanation:
- The score of node 0 is: 2 = 2
- The score of node 1 is: 2 = 2
- The score of node 2 is: 1 * 1 = 1
The highest score is 2, and two nodes (node 0 and node 1) have the highest score.
Constraints:
n == parents.length2 <= n <= 105parents[0] == -10 <= parents[i] <= n - 1fori != 0parentsrepresents a valid binary tree.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach iterates through each node of the tree. For each node, it simulates its removal and then performs a graph traversal (like DFS or BFS) on the remaining parts to identify the resulting subtrees. The sizes of these components are multiplied to get the score for the current node. This process is repeated for all nodes, keeping track of the highest score and the count of nodes achieving it.
Algorithm
- Build an adjacency list
adjfrom theparentsarray to represent the tree structure, whereadj[i]contains the children of nodei. - Define a helper function,
getSubtreeSize(startNode, adj), which performs a traversal (like DFS or BFS) starting fromstartNodeto count all nodes in its subtree. This function takes O(N) time in the worst case. - Initialize
maxScore = -1Landcount = 0. - Iterate through each node
ifrom0ton-1. - For each node
i, calculate its score:- Initialize
currentScore = 1LandnodesBelow = 0. - For each
childof nodeiinadj[i]:- Call
getSubtreeSize(child, adj)to get the size of the child's subtree. - Multiply
currentScoreby this size. - Add this size to
nodesBelow.
- Call
- Calculate the size of the component 'above' node
i. This isparentComponentSize = n - 1 - nodesBelow. - If
parentComponentSizeis greater than 0, multiplycurrentScoreby it.
- Initialize
- Compare
currentScorewithmaxScore.- If
currentScore > maxScore, updatemaxScore = currentScoreand setcount = 1. - If
currentScore == maxScore, incrementcount.
- If
- After the loop, return
count.
Walkthrough
The brute-force method directly implements the score calculation for each node one by one. First, we convert the parents array into a more usable tree structure, like an adjacency list. Then, we loop through every node i from 0 to n-1. For each i, we need to find the sizes of the components that would form if i were removed. These components are the subtrees of i's children and the rest of the tree (containing i's parent).
To find the size of each child's subtree, we can perform a separate traversal (like BFS or DFS) starting from that child. After calculating the sizes of all children's subtrees, we sum them up. The size of the parent component is then n minus 1 (for node i itself) minus the sum of its children's subtree sizes. The score is the product of these component sizes. We use a long to store the score to avoid overflow. We keep track of the maximum score seen so far and how many nodes achieve it.
import java.util.*; class Solution { public int countHighestScoreNodes(int[] parents) { int n = parents.length; List<Integer>[] adj = new ArrayList[n]; for (int i = 0; i < n; i++) { adj[i] = new ArrayList<>(); } for (int i = 1; i < n; i++) { adj[parents[i]].add(i); } long maxScore = -1; int count = 0; for (int i = 0; i < n; i++) { long currentScore = 1; int nodesBelow = 0; for (int child : adj[i]) { int childSubtreeSize = getSubtreeSize(child, adj); currentScore *= childSubtreeSize; nodesBelow += childSubtreeSize; } int parentComponentSize = n - 1 - nodesBelow; if (parentComponentSize > 0) { currentScore *= parentComponentSize; } if (currentScore > maxScore) { maxScore = currentScore; count = 1; } else if (currentScore == maxScore) { count++; } } return count; } private int getSubtreeSize(int startNode, List<Integer>[] adj) { int size = 0; Queue<Integer> queue = new LinkedList<>(); queue.offer(startNode); while (!queue.isEmpty()) { int u = queue.poll(); size++; for (int v : adj[u]) { queue.offer(v); } } return size; }}Complexity
Time
O(N^2). The main loop runs N times. Inside the loop, for each child of the current node, we call `getSubtreeSize`. In the worst case (a skewed tree), `getSubtreeSize` can take O(N) time. Since this is inside a loop that runs N times, the total time complexity is O(N^2).
Space
O(N) to store the adjacency list and for the queue/stack used in the traversal within the loop.
Trade-offs
Pros
Conceptually simple and directly follows the problem definition.
Cons
Highly inefficient due to redundant calculations. The size of the same subtree is computed multiple times.
The time complexity of O(N^2) can be too slow for the given constraints (N up to 10^5), likely resulting in a 'Time Limit Exceeded' error.
This optimal approach avoids redundant computations by using a single Depth First Search (DFS) traversal. It calculates the size of each node's subtree in a post-order manner. Once the sizes of a node's children subtrees are known (after the recursive calls for its children return), we can immediately calculate the size of the parent component and thus the node's score. This combines subtree size calculation and score evaluation into one efficient pass.
Algorithm
- Build an adjacency list
adjfrom theparentsarray. - Initialize global variables
maxScore = -1Landcount = 0. - Create a recursive DFS function,
dfs(node), that returns the size of the subtree rooted atnode. - Inside
dfs(node):- Initialize
mySize = 1(for the node itself) andscore = 1L. - For each
childofnode:- Recursively call
childSize = dfs(child). - Multiply
scorebychildSize. - Add
childSizetomySize.
- Recursively call
- Calculate the size of the parent component:
parentSize = n - mySize. - If
parentSize > 0, multiplyscorebyparentSize. - Compare the calculated
scorewith the globalmaxScore. UpdatemaxScoreandcountaccordingly. - Return
mySize.
- Initialize
- Start the process by calling
dfs(0)on the root node. - After the traversal is complete, return the final
count.
Walkthrough
The key to an efficient solution is to avoid re-calculating subtree sizes. A single post-order traversal of the tree can achieve this. We define a recursive function, let's call it dfs(node), which will compute and return the size of the subtree rooted at node.
During the traversal, after visiting all of a node's children and getting their subtree sizes from the recursive calls, we are at the 'post-order' step for the current node. At this point, we have all the information needed to calculate its score:
- The sizes of the subtrees for each of its children.
- The size of the current node's own subtree, which is 1 (for itself) plus the sum of its children's subtree sizes.
- The size of the parent component, which is the total number of nodes
nminus the size of the current node's subtree.
The score is the product of these component sizes. We must handle cases where a component might be empty (e.g., a leaf has no children, the root has no parent). A simple way is to multiply by a component's size only if it's greater than zero. We use a long for the score to prevent overflow. As we compute the score for each node during the traversal, we update a global maximum score and the count of nodes achieving it.
import java.util.*; class Solution { private long maxScore; private int count; private int n; private List<Integer>[] adj; public int countHighestScoreNodes(int[] parents) { this.n = parents.length; this.adj = new ArrayList[n]; for (int i = 0; i < n; i++) { adj[i] = new ArrayList<>(); } for (int i = 1; i < n; i++) { adj[parents[i]].add(i); } this.maxScore = -1; this.count = 0; dfs(0); return count; } private int dfs(int node) { long score = 1; int subtreeSize = 1; for (int child : adj[node]) { int childSubtreeSize = dfs(child); score *= childSubtreeSize; subtreeSize += childSubtreeSize; } int parentComponentSize = n - subtreeSize; if (parentComponentSize > 0) { score *= parentComponentSize; } if (score > maxScore) { maxScore = score; count = 1; } else if (score == maxScore) { count++; } return subtreeSize; }}Complexity
Time
O(N). The DFS-based approach visits each node and edge of the tree exactly once. All operations performed at each node are constant time (excluding the recursive calls).
Space
O(N). This space is used for the adjacency list and the recursion call stack. In the worst-case scenario of a skewed tree, the recursion depth can be O(N).
Trade-offs
Pros
Extremely efficient with optimal time complexity.
Solves the problem in a single pass over the tree, avoiding any redundant work.
Elegant and concise implementation using recursion.
Cons
The recursive implementation might lead to a
StackOverflowErrorfor extremely deep, skewed trees, although this is unlikely given the problem constraints and typical system stack sizes.
Solutions
Solution
public class Solution { private List < int > [] g; private int ans; private long mx; private int n; public int CountHighestScoreNodes(int[] parents) { n = parents.Length; g = new List < int > [n]; for (int i = 0; i < n; ++i) { g[i] = new List < int > (); } for (int i = 1; i < n; ++i) { g[parents[i]].Add(i); } Dfs(0, -1); return ans; } private int Dfs(int i, int fa) { int cnt = 1; long score = 1; foreach(int j in g[i]) { if (j != fa) { int t = Dfs(j, i); cnt += t; score *= t; } } if (n - cnt > 0) { score *= n - cnt; } if (mx < score) { mx = score; ans = 1; } else if (mx == score) { ++ans; } return cnt; }}class Solution {private List<Integer>[] g;private int ans;private long mx;private int n;public int countHighestScoreNodes(int[] parents) { n = parents.length; g = new List[n]; Arrays.setAll(g, i->new ArrayList<>()); for (int i = 1; i < n; ++i) { g[parents[i]].add(i); } dfs(0, -1); return ans; }private int dfs(int i, int fa) { int cnt = 1; long score = 1; for (int j : g[i]) { if (j != fa) { int t = dfs(j, i); cnt += t; score *= t; } } if (n - cnt > 0) { score *= n - cnt; } if (mx < score) { mx = score; ans = 1; } else if (mx == score) { ++ans; } return cnt; }}class Solution {public: int countHighestScoreNodes(vector<int> &parents) { int n = parents.size(); vector<int> g[n]; for (int i = 1; i < n; ++i) { g[parents[i]].push_back(i); } int ans = 0; long long mx = 0; function<int(int, int)> dfs = [&](int i, int fa) { long long score = 1; int cnt = 1; for (int j : g[i]) { if (j != fa) { int t = dfs(j, i); cnt += t; score *= t; } } if (n - cnt) { score *= n - cnt; } if (mx < score) { mx = score; ans = 1; } else if (mx == score) { ++ans; } return cnt; }; dfs(0, -1); return ans; }};class Solution: def countHighestScoreNodes(self, parents: List[int]) -> int: def dfs(i: int, fa: int): cnt = score = 1 for j in g[i]: if j != fa: t = dfs(j, i) score *= t cnt += t if n - cnt: score *= n - cnt nonlocal ans, mx if mx < score: mx = score ans = 1 elif mx == score: ans += 1 return cnt n = len(parents) g = [[] for _ in range(n)] for i in range(1, n): g[parents[i]]. append(i) ans = mx = 0 dfs(0, - 1) 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.