2049. Count Nodes With the Highest Score
Problem Description
You are given a binary tree with n
nodes labeled from 0
to n - 1
, where node 0
is the root. The tree structure is represented by a 0-indexed array parents
, where parents[i]
indicates the parent of node i
. Since node 0
is the root, parents[0] == -1
.
The task is to calculate a score for each node in the tree. To find the score of a particular node:
- Imagine removing that node and all edges connected to it from the tree
- This removal will split the tree into one or more non-empty subtrees
- The score of the node equals the product of the sizes of all these resulting subtrees
For example, if removing a node creates three subtrees with sizes 2, 3, and 4 nodes respectively, the score of that node would be 2 × 3 × 4 = 24
.
Your goal is to find how many nodes have the highest score among all nodes in the tree.
Key Points:
- When a node is removed, the tree splits into separate components
- Each component's size is the number of nodes it contains
- The score is the product of all component sizes
- You need to count how many nodes achieve the maximum score
The solution uses DFS to traverse the tree and calculate each node's score by:
- Computing the size of each child subtree
- Computing the size of the "parent subtree" (if the node isn't the root)
- Multiplying all these sizes together to get the score
- Tracking the maximum score and counting how many nodes achieve it
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem involves a tree structure where nodes are connected by parent-child relationships. A tree is a special type of graph (specifically, a connected acyclic graph).
Is it a tree?
- Yes: The problem explicitly states we have a binary tree rooted at node 0, with parent-child relationships defined by the
parents
array.
DFS
- Yes: Since we confirmed it's a tree problem, the flowchart leads us directly to DFS (Depth-First Search).
Conclusion: The flowchart suggests using a DFS approach for this problem.
This makes perfect sense because:
- We need to traverse the entire tree to calculate scores for each node
- For each node, we need to know the sizes of subtrees formed when that node is removed
- DFS naturally computes subtree sizes during its traversal - when we visit a node, we can recursively calculate the size of its subtrees
- The score calculation requires knowing both the sizes of child subtrees (computed during DFS traversal) and the size of the "parent component" (which is
n - current_subtree_size
)
The DFS approach allows us to efficiently compute all node scores in a single tree traversal, making it the ideal algorithm for this problem.
Intuition
When we remove a node from a tree, we're essentially "cutting" the tree at that point. This creates multiple disconnected components - the key insight is understanding what these components are:
- Child subtrees: Each child of the removed node becomes the root of its own separate subtree
- Parent component: Everything "above" the removed node (including siblings and their descendants) forms another component
To calculate a node's score, we need the sizes of all these components and multiply them together. The challenge is: how do we efficiently find these sizes for every node?
Here's where DFS becomes powerful. When we traverse a tree using DFS, we naturally visit all nodes in a subtree before returning to the parent. This means that as we return from a recursive call, we can carry back information about the subtree size.
Think of it like this: if we start DFS from the root and recursively explore each child, when the recursion "bubbles up" from a child, it can tell us "hey, my subtree has X nodes." This is exactly the information we need!
For any node i
:
- When we visit its children through DFS, each child returns the size of its subtree
- These are the sizes of components that would be created if we removed node
i
- The remaining nodes (those not in any child subtree of
i
) form the "parent component" with sizen - total_nodes_in_subtree_of_i
The beauty of this approach is that we can calculate every node's score in a single tree traversal. As we visit each node:
- Recursively get the sizes of all child subtrees
- Calculate the parent component size as
n - current_subtree_size
- Multiply all these sizes together to get the score
- Track the maximum score and count how many nodes achieve it
This way, we solve the entire problem with just one DFS traversal, making it both elegant and efficient.
Learn more about Tree, Depth-First Search and Binary Tree patterns.
Solution Approach
The implementation follows the DFS strategy we identified, with careful tracking of subtree sizes and score calculations.
Building the Tree Structure:
First, we construct an adjacency list representation of the tree from the parents
array:
g = [[] for _ in range(n)]
for i in range(1, n):
g[parents[i]].append(i)
This creates a graph where g[i]
contains all direct children of node i
. Note that we only store parent-to-child edges since we'll traverse from root downward.
DFS Function Design:
The core logic lies in the dfs(i, fa)
function where:
i
is the current node being processedfa
is the parent of nodei
(used to avoid revisiting the parent)- Returns the size of the subtree rooted at node
i
Calculating Subtree Sizes and Scores:
For each node i
, we initialize:
cnt = 1
: counts nodes in the current subtree (starting with the node itself)score = 1
: accumulates the product of component sizes
As we traverse each child j
of node i
:
for j in g[i]: if j != fa: # Skip the parent to avoid cycles t = dfs(j, i) # Get child subtree size score *= t # Multiply into the score cnt += t # Add to current subtree size
Handling the Parent Component:
After processing all children, if there are nodes outside the current subtree (i.e., n - cnt > 0
), these form the parent component:
if n - cnt: score *= n - cnt
This accounts for all nodes "above" the current node in the tree.
Tracking Maximum Score: We use two global variables:
mx
: stores the highest score found so farans
: counts how many nodes achieve this highest score
if mx < score: mx = score ans = 1 elif mx == score: ans += 1
Complete Traversal:
The algorithm starts with dfs(0, -1)
from the root (node 0 with no parent), ensuring every node is visited exactly once. Each node's score is calculated using the sizes returned by its children's recursive calls plus the parent component size.
The time complexity is O(n)
since we visit each node once, and the space complexity is O(n)
for the recursion stack and adjacency list storage.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Consider a tree with 5 nodes where parents = [-1, 0, 0, 1, 1]
:
Tree structure: 0 (root) / \ 1 2 / \ 3 4
Step 1: Build adjacency list
g[0] = [1, 2] // node 0 has children 1 and 2 g[1] = [3, 4] // node 1 has children 3 and 4 g[2] = [] // node 2 is a leaf g[3] = [] // node 3 is a leaf g[4] = [] // node 4 is a leaf
Step 2: DFS traversal starting from root
Let's trace through the DFS calls and see how scores are calculated:
Processing Node 3 (leaf):
- No children to process
- Subtree size = 1
- Parent component size = 5 - 1 = 4
- Score = 4
- Returns size 1 to parent
Processing Node 4 (leaf):
- No children to process
- Subtree size = 1
- Parent component size = 5 - 1 = 4
- Score = 4
- Returns size 1 to parent
Processing Node 1:
- Child 3 returns size 1 → score = 1
- Child 4 returns size 1 → score = 1 × 1 = 1
- Subtree size = 1 + 1 + 1 = 3
- Parent component size = 5 - 3 = 2
- Score = 1 × 1 × 2 = 2
- Returns size 3 to parent
Processing Node 2 (leaf):
- No children to process
- Subtree size = 1
- Parent component size = 5 - 1 = 4
- Score = 4
- Returns size 1 to parent
Processing Node 0 (root):
- Child 1 returns size 3 → score = 3
- Child 2 returns size 1 → score = 3 × 1 = 3
- Subtree size = 1 + 3 + 1 = 5
- Parent component size = 5 - 5 = 0 (no parent component for root)
- Score = 3 × 1 = 3
Step 3: Track maximum scores
Final scores for each node:
- Node 0: score = 3
- Node 1: score = 2
- Node 2: score = 4
- Node 3: score = 4
- Node 4: score = 4
Maximum score = 4, achieved by nodes 2, 3, and 4. Answer: 3 nodes have the highest score.
Verification of scores:
- Removing node 2: Creates one component of size 4 → score = 4 ✓
- Removing node 3: Creates one component of size 4 → score = 4 ✓
- Removing node 4: Creates one component of size 4 → score = 4 ✓
- Removing node 1: Creates components of sizes 1, 1, and 2 → score = 1×1×2 = 2 ✓
- Removing node 0: Creates components of sizes 3 and 1 → score = 3×1 = 3 ✓
This walkthrough demonstrates how the DFS approach efficiently calculates each node's score by using subtree sizes returned from recursive calls and computing parent component sizes as n - subtree_size
.
Solution Implementation
1class Solution:
2 def countHighestScoreNodes(self, parents: List[int]) -> int:
3 # Build adjacency list representation of the tree
4 n = len(parents)
5 adjacency_list = [[] for _ in range(n)]
6 for child in range(1, n):
7 parent = parents[child]
8 adjacency_list[parent].append(child)
9
10 # Initialize variables to track maximum score and count
11 self.max_score = 0
12 self.count_max_score = 0
13
14 def calculate_subtree_sizes(node: int, parent: int) -> int:
15 """
16 DFS to calculate subtree sizes and compute score for removing each node.
17 Returns the size of subtree rooted at current node.
18 """
19 subtree_size = 1 # Count current node
20 node_score = 1 # Product of all component sizes when removing this node
21
22 # Process all children (excluding parent to avoid revisiting)
23 for child in adjacency_list[node]:
24 if child != parent:
25 # Recursively get child's subtree size
26 child_subtree_size = calculate_subtree_sizes(child, node)
27 # Multiply score by this component's size
28 node_score *= child_subtree_size
29 # Add to current subtree size
30 subtree_size += child_subtree_size
31
32 # If there's a parent component (nodes above current node)
33 parent_component_size = n - subtree_size
34 if parent_component_size > 0:
35 node_score *= parent_component_size
36
37 # Update global maximum score and count
38 if node_score > self.max_score:
39 self.max_score = node_score
40 self.count_max_score = 1
41 elif node_score == self.max_score:
42 self.count_max_score += 1
43
44 return subtree_size
45
46 # Start DFS from root (node 0), with no parent (-1)
47 calculate_subtree_sizes(0, -1)
48
49 return self.count_max_score
50
1class Solution {
2 // Adjacency list to represent the tree structure
3 private List<Integer>[] adjacencyList;
4 // Count of nodes with the highest score
5 private int countOfMaxScore;
6 // Maximum score found so far
7 private long maxScore;
8 // Total number of nodes in the tree
9 private int totalNodes;
10
11 public int countHighestScoreNodes(int[] parents) {
12 // Initialize the total number of nodes
13 totalNodes = parents.length;
14
15 // Initialize the adjacency list for the tree
16 adjacencyList = new List[totalNodes];
17 Arrays.setAll(adjacencyList, index -> new ArrayList<>());
18
19 // Build the tree by adding children to their respective parents
20 // Start from index 1 since node 0 is the root (has no parent)
21 for (int childIndex = 1; childIndex < totalNodes; childIndex++) {
22 adjacencyList[parents[childIndex]].add(childIndex);
23 }
24
25 // Perform DFS starting from the root node (index 0)
26 dfs(0, -1);
27
28 return countOfMaxScore;
29 }
30
31 /**
32 * Performs depth-first search to calculate scores for each node
33 * @param currentNode - the current node being processed
34 * @param parentNode - the parent of the current node (-1 for root)
35 * @return the size of the subtree rooted at currentNode
36 */
37 private int dfs(int currentNode, int parentNode) {
38 // Initialize subtree size (including current node)
39 int subtreeSize = 1;
40
41 // Initialize score for removing current node
42 // Score is the product of all connected component sizes
43 long nodeScore = 1;
44
45 // Process all children of the current node
46 for (int childNode : adjacencyList[currentNode]) {
47 // Skip if this is the parent node (avoid going back up the tree)
48 if (childNode != parentNode) {
49 // Recursively calculate the size of child's subtree
50 int childSubtreeSize = dfs(childNode, currentNode);
51
52 // Add child's subtree size to current subtree
53 subtreeSize += childSubtreeSize;
54
55 // Multiply score by the size of this child's subtree
56 nodeScore *= childSubtreeSize;
57 }
58 }
59
60 // Calculate the size of the remaining tree (nodes above current node)
61 // This represents the component containing the parent
62 int remainingTreeSize = totalNodes - subtreeSize;
63 if (remainingTreeSize > 0) {
64 // Multiply score by the size of the parent's component
65 nodeScore *= remainingTreeSize;
66 }
67
68 // Update the maximum score and count
69 if (maxScore < nodeScore) {
70 // Found a new maximum score
71 maxScore = nodeScore;
72 countOfMaxScore = 1;
73 } else if (maxScore == nodeScore) {
74 // Found another node with the same maximum score
75 countOfMaxScore++;
76 }
77
78 // Return the size of the subtree rooted at current node
79 return subtreeSize;
80 }
81}
82
1class Solution {
2public:
3 int countHighestScoreNodes(vector<int>& parents) {
4 int n = parents.size();
5
6 // Build adjacency list representation of the tree
7 // Each index represents a node, and the vector contains its children
8 vector<vector<int>> children(n);
9 for (int i = 1; i < n; ++i) {
10 children[parents[i]].push_back(i);
11 }
12
13 int nodesWithMaxScore = 0;
14 long long maxScore = 0;
15
16 // DFS function to calculate subtree sizes and scores
17 // Returns the size of subtree rooted at current node
18 function<int(int)> calculateSubtreeSize = [&](int currentNode) {
19 long long currentScore = 1;
20 int subtreeSize = 1; // Count current node
21
22 // Process all children of current node
23 for (int childNode : children[currentNode]) {
24 int childSubtreeSize = calculateSubtreeSize(childNode);
25 subtreeSize += childSubtreeSize;
26
27 // When removing current node, child subtree becomes a separate component
28 currentScore *= childSubtreeSize;
29 }
30
31 // Calculate the size of the remaining tree (parent's side)
32 // when current node is removed
33 int parentSideSize = n - subtreeSize;
34 if (parentSideSize > 0) {
35 currentScore *= parentSideSize;
36 }
37
38 // Update the count of nodes with maximum score
39 if (currentScore > maxScore) {
40 maxScore = currentScore;
41 nodesWithMaxScore = 1;
42 } else if (currentScore == maxScore) {
43 ++nodesWithMaxScore;
44 }
45
46 return subtreeSize;
47 };
48
49 // Start DFS from root node (node 0)
50 calculateSubtreeSize(0);
51
52 return nodesWithMaxScore;
53 }
54};
55
1/**
2 * Counts the number of nodes that have the highest score when removed from a tree.
3 * The score of removing a node is the product of the sizes of all resulting subtrees.
4 * @param parents - Array where parents[i] is the parent of node i (parents[0] = -1 for root)
5 * @returns The count of nodes with the highest score
6 */
7function countHighestScoreNodes(parents: number[]): number {
8 const nodeCount: number = parents.length;
9
10 // Build adjacency list representation of the tree
11 const adjacencyList: number[][] = Array.from({ length: nodeCount }, () => []);
12
13 // Construct the tree by adding edges from parent to child
14 for (let childNode = 1; childNode < nodeCount; childNode++) {
15 adjacencyList[parents[childNode]].push(childNode);
16 }
17
18 let highestScoreNodeCount: number = 0;
19 let maxScore: number = 0;
20
21 /**
22 * Performs DFS to calculate subtree sizes and scores for each node
23 * @param currentNode - The current node being processed
24 * @param parentNode - The parent of the current node (-1 for root)
25 * @returns The size of the subtree rooted at currentNode
26 */
27 const calculateSubtreeScores = (currentNode: number, parentNode: number): number => {
28 let subtreeSize: number = 1; // Count current node
29 let nodeScore: number = 1; // Initialize score as product
30
31 // Process all children of current node
32 for (const childNode of adjacencyList[currentNode]) {
33 if (childNode !== parentNode) {
34 const childSubtreeSize: number = calculateSubtreeScores(childNode, currentNode);
35 subtreeSize += childSubtreeSize;
36 nodeScore *= childSubtreeSize; // Multiply score by child subtree size
37 }
38 }
39
40 // If there's a parent subtree (nodes above current node), multiply by its size
41 const parentSubtreeSize: number = nodeCount - subtreeSize;
42 if (parentSubtreeSize > 0) {
43 nodeScore *= parentSubtreeSize;
44 }
45
46 // Update the count of nodes with highest score
47 if (nodeScore > maxScore) {
48 maxScore = nodeScore;
49 highestScoreNodeCount = 1;
50 } else if (nodeScore === maxScore) {
51 highestScoreNodeCount++;
52 }
53
54 return subtreeSize;
55 };
56
57 // Start DFS from root node (node 0)
58 calculateSubtreeScores(0, -1);
59
60 return highestScoreNodeCount;
61}
62
Time and Space Complexity
Time Complexity: O(n)
The algorithm performs a depth-first search (DFS) traversal of the tree. Each node is visited exactly once during the DFS traversal. At each node, the algorithm:
- Iterates through all children of the current node (each edge is traversed once)
- Performs constant time operations (multiplication, comparison, addition)
Since we visit each of the n
nodes exactly once and process each edge exactly once (there are n-1
edges in a tree), the total time complexity is O(n)
.
Space Complexity: O(n)
The space complexity consists of:
- The adjacency list
g
which stores all edges:O(n)
space (storingn-1
edges acrossn
lists) - The recursive call stack for DFS:
O(h)
whereh
is the height of the tree. In the worst case (skewed tree),h = n
, givingO(n)
space - A few variables (
ans
,mx
,cnt
,score
, etc.):O(1)
space
The dominant factor is the adjacency list and the potential recursive stack depth, resulting in an overall space complexity of O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Overflow with Large Trees
The most critical pitfall in this problem is integer overflow when calculating the product of component sizes. For a tree with up to 10^5 nodes, the product can exceed standard integer limits.
Example scenario: Consider a star-shaped tree where the root has many children, each being a leaf. When removing the root, if there are 10^5 - 1 leaf nodes, the score would be 1^(10^5-1), which technically equals 1, but intermediate calculations with larger subtree sizes could overflow.
Solution: Python handles arbitrary precision integers automatically, but in languages like Java or C++, use long long or BigInteger types:
# Python handles this automatically node_score *= child_subtree_size # No overflow issues # In Java, you'd need: # long nodeScore = 1L; # nodeScore *= childSubtreeSize;
2. Forgetting to Handle Leaf Nodes Correctly
A common mistake is not properly handling leaf nodes, which have no children. When a leaf node is removed, the only remaining component is the rest of the tree (size n-1), so the score should be n-1.
Incorrect approach:
# Wrong: Forgetting that leaf nodes still have a parent component if not adjacency_list[node]: # Leaf node node_score = 0 # WRONG!
Correct approach:
# The existing code handles this correctly: if parent_component_size > 0: # This covers leaf nodes node_score *= parent_component_size # Score = n-1 for leaves
3. Incorrectly Building the Adjacency List
Some might accidentally create bidirectional edges or miss the parent-child relationship:
Incorrect:
# Wrong: Creating bidirectional edges
for i in range(1, n):
adjacency_list[parents[i]].append(i)
adjacency_list[i].append(parents[i]) # WRONG - creates cycles!
Correct:
# Only store parent-to-child edges
for child in range(1, n):
parent = parents[child]
adjacency_list[parent].append(child)
4. Not Initializing Score to 1
A subtle but critical error is initializing the node_score to 0 instead of 1:
Incorrect:
node_score = 0 # WRONG - multiplying by anything gives 0 for child in adjacency_list[node]: node_score *= child_subtree_size # Always 0!
Correct:
node_score = 1 # Correct - identity element for multiplication
5. Forgetting Edge Case of Single Node Tree
When n=1 (single node tree), removing the only node leaves no components. The score should be 1 (empty product convention), not 0.
The solution handles this correctly: When n=1, no children exist, and parent_component_size = 1-1 = 0, so the if condition if parent_component_size > 0
is false, leaving node_score = 1.
Which data structure is used in a depth first search?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
Binary Tree Min Depth Prereq BFS on Tree problems bfs_intro Given a binary tree find the depth of the shallowest leaf node https assets algo monster binary_tree_min_depth png Explanation We can solve this problem with either DFS or BFS With DFS we traverse the whole tree looking for leaf nodes and record and update the minimum depth as we go With BFS though since we search level by level we are guaranteed to find the shallowest leaf node
Want a Structured Path to Master System Design Too? Don’t Miss This!