1120. Maximum Average Subtree 🔒
Problem Description
Given the root of a binary tree, you need to find the maximum average value among all possible subtrees in that tree.
A subtree is defined as any node in the tree along with all of its descendants. Every node by itself (with no children) also counts as a subtree.
The average value of a subtree is calculated by taking the sum of all node values in that subtree and dividing it by the total number of nodes in that subtree.
For example, if a subtree contains nodes with values [5, 6, 1]
, the average would be (5 + 6 + 1) / 3 = 4
.
Your task is to examine all possible subtrees in the given binary tree and return the highest average value found. The answer will be accepted if it's within 10^-5
of the actual answer (to account for floating-point precision).
The solution uses a depth-first search (DFS) approach where for each node, it calculates:
- The sum of all values in the subtree rooted at that node
- The count of nodes in that subtree
- The average value (sum/count) for comparison with the current maximum
The function dfs(root)
returns a tuple (sum, count)
for each subtree, allowing the parent nodes to aggregate these values. As we traverse each node, we calculate its subtree's average and update the global maximum if a higher average is found.
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 binary tree, which is a special type of graph (specifically, a connected acyclic graph).
Is it a tree?
- Yes: The problem explicitly states we're working with a binary tree structure where we need to examine all subtrees.
DFS
- We arrive at DFS: Since we're dealing with a tree and need to explore all subtrees (every node and its descendants), DFS is the natural choice.
Why DFS is appropriate here:
- We need to visit every node in the tree to consider it as a potential subtree root
- For each node, we need information from its children before we can calculate the subtree's average (post-order traversal)
- DFS allows us to recursively calculate the sum and count of nodes in each subtree from bottom to top
- The recursive nature of DFS perfectly matches the recursive structure of the tree
Conclusion: The flowchart correctly leads us to DFS for this problem. The DFS pattern is ideal because we need to:
- Traverse the entire tree to examine all possible subtrees
- Aggregate information (sum and count) from child nodes before processing parent nodes
- Calculate averages for each subtree during the traversal
- Track the maximum average found across all subtrees
Intuition
To find the maximum average among all subtrees, we need to calculate the average for every possible subtree in the tree. A key observation is that to calculate the average of any subtree, we need two pieces of information: the sum of all node values in that subtree and the count of nodes in that subtree.
Think about how we can get this information efficiently. If we're at a node, we can calculate its subtree's sum and count if we know:
- The sum and count from its left subtree
- The sum and count from its right subtree
- Its own value (which adds 1 to the count and
node.val
to the sum)
This naturally suggests a bottom-up approach where we first process the children before processing the parent. This is exactly what post-order DFS gives us.
For each node during our traversal:
- We recursively get
(sum, count)
from the left child - We recursively get
(sum, count)
from the right child - We calculate the current subtree's total sum:
current_sum = node.val + left_sum + right_sum
- We calculate the current subtree's total count:
current_count = 1 + left_count + right_count
- We compute the average:
average = current_sum / current_count
- We update our global maximum if this average is larger
The beauty of this approach is that we calculate each subtree's average exactly once while traversing the tree, making it efficient with O(n)
time complexity. By returning (sum, count)
from each recursive call, parent nodes can easily aggregate the information from their children without recalculating anything.
The base case is straightforward: a null
node contributes 0 to both sum and count, represented as (0, 0)
.
Learn more about Tree, Depth-First Search and Binary Tree patterns.
Solution Approach
The implementation uses a recursive DFS approach with a helper function that returns two values for each subtree: the sum of node values and the count of nodes.
Implementation Details:
-
Main Function Setup:
- Initialize a variable
ans
to track the maximum average found so far - Call the DFS helper function starting from the root
- Return the final maximum average
- Initialize a variable
-
DFS Helper Function
dfs(root)
:-
Base Case: If the current node is
None
, return(0, 0)
representing zero sum and zero count -
Recursive Calls:
- Call
dfs(root.left)
to get the sum and count from the left subtree:ls, ln
- Call
dfs(root.right)
to get the sum and count from the right subtree:rs, rn
- Call
-
Calculate Current Subtree Values:
- Sum of current subtree:
s = root.val + ls + rs
- Count of nodes in current subtree:
n = 1 + ln + rn
- Average of current subtree:
s / n
- Sum of current subtree:
-
Update Maximum:
- Compare the current subtree's average with the global maximum
- Update using:
ans = max(ans, s / n)
-
Return Values:
- Return
(s, n)
so parent nodes can use these values in their calculations
- Return
-
Key Design Choices:
-
Using
nonlocal
: Theans
variable is declared asnonlocal
inside the DFS function to allow modification of the outer scope variable, enabling us to track the global maximum across all recursive calls -
Post-order Traversal: The algorithm processes children before parents (left → right → root), ensuring we have all necessary information from descendants before calculating a node's subtree average
-
Single Pass: Each node is visited exactly once, and its subtree information is calculated only once, achieving
O(n)
time complexity
Time and Space Complexity:
- Time:
O(n)
wheren
is the number of nodes, as we visit each node exactly once - Space:
O(h)
whereh
is the height of the tree, due to the recursive call stack
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 concrete example to understand how the solution works.
Consider this binary tree:
5 / \ 6 1 / 2
We want to find the maximum average among all possible subtrees.
Step-by-step DFS traversal (post-order):
-
Start at root (5), but first we need to process its children.
-
Go to left child (6), but first process its children.
-
Go to node 2 (leaf):
- Left child:
None
→ returns(0, 0)
- Right child:
None
→ returns(0, 0)
- Current sum:
2 + 0 + 0 = 2
- Current count:
1 + 0 + 0 = 1
- Average:
2/1 = 2.0
- Update max:
ans = 2.0
- Return:
(2, 1)
- Left child:
-
Back to node 6:
- Left child returned:
(2, 1)
- Right child:
None
→ returns(0, 0)
- Current sum:
6 + 2 + 0 = 8
- Current count:
1 + 1 + 0 = 2
- Average:
8/2 = 4.0
- Update max:
ans = 4.0
(since 4.0 > 2.0) - Return:
(8, 2)
- Left child returned:
-
Go to right child (1):
- Left child:
None
→ returns(0, 0)
- Right child:
None
→ returns(0, 0)
- Current sum:
1 + 0 + 0 = 1
- Current count:
1 + 0 + 0 = 1
- Average:
1/1 = 1.0
- Max stays:
ans = 4.0
(since 1.0 < 4.0) - Return:
(1, 1)
- Left child:
-
Back to root (5):
- Left child returned:
(8, 2)
- Right child returned:
(1, 1)
- Current sum:
5 + 8 + 1 = 14
- Current count:
1 + 2 + 1 = 4
- Average:
14/4 = 3.5
- Max stays:
ans = 4.0
(since 3.5 < 4.0) - Return:
(14, 4)
- Left child returned:
Result: The maximum average is 4.0
, found in the subtree rooted at node 6 (containing nodes 6 and 2).
All subtrees and their averages:
- Subtree rooted at 2: avg = 2/1 = 2.0
- Subtree rooted at 6: avg = 8/2 = 4.0 ✓ (maximum)
- Subtree rooted at 1: avg = 1/1 = 1.0
- Subtree rooted at 5: avg = 14/4 = 3.5
The algorithm efficiently computes each subtree's average exactly once by propagating sum and count values up the tree, allowing parent nodes to reuse calculations from their children.
Solution Implementation
1# Definition for a binary tree node.
2# class TreeNode:
3# def __init__(self, val=0, left=None, right=None):
4# self.val = val
5# self.left = left
6# self.right = right
7
8from typing import Optional
9
10class Solution:
11 def maximumAverageSubtree(self, root: Optional[TreeNode]) -> float:
12 """
13 Find the maximum average value of any subtree in the binary tree.
14 A subtree average is the sum of all node values divided by the number of nodes.
15 """
16
17 def dfs(node: Optional[TreeNode]) -> tuple[int, int]:
18 """
19 Perform depth-first search to calculate sum and count for each subtree.
20
21 Args:
22 node: Current tree node being processed
23
24 Returns:
25 tuple: (sum of subtree values, count of nodes in subtree)
26 """
27 # Base case: empty node contributes 0 sum and 0 nodes
28 if node is None:
29 return 0, 0
30
31 # Recursively get sum and count from left subtree
32 left_sum, left_count = dfs(node.left)
33
34 # Recursively get sum and count from right subtree
35 right_sum, right_count = dfs(node.right)
36
37 # Calculate current subtree's sum and node count
38 subtree_sum = node.val + left_sum + right_sum
39 subtree_count = 1 + left_count + right_count
40
41 # Update maximum average if current subtree has higher average
42 nonlocal max_average
43 current_average = subtree_sum / subtree_count
44 max_average = max(max_average, current_average)
45
46 # Return sum and count for parent's calculation
47 return subtree_sum, subtree_count
48
49 # Initialize maximum average to track the best result
50 max_average = 0.0
51
52 # Start DFS traversal from root
53 dfs(root)
54
55 # Return the maximum average found
56 return max_average
57
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 * int val;
5 * TreeNode left;
6 * TreeNode right;
7 * TreeNode() {}
8 * TreeNode(int val) { this.val = val; }
9 * TreeNode(int val, TreeNode left, TreeNode right) {
10 * this.val = val;
11 * this.left = left;
12 * this.right = right;
13 * }
14 * }
15 */
16class Solution {
17 // Global variable to track the maximum average found
18 private double maxAverage;
19
20 /**
21 * Finds the maximum average value of all subtrees in the binary tree.
22 *
23 * @param root The root of the binary tree
24 * @return The maximum average value among all subtrees
25 */
26 public double maximumAverageSubtree(TreeNode root) {
27 maxAverage = 0.0;
28 calculateSubtreeInfo(root);
29 return maxAverage;
30 }
31
32 /**
33 * Performs post-order traversal to calculate sum and count of nodes for each subtree.
34 * Updates the maximum average during traversal.
35 *
36 * @param node The current node being processed
37 * @return An array where [0] = sum of subtree values, [1] = count of nodes in subtree
38 */
39 private int[] calculateSubtreeInfo(TreeNode node) {
40 // Base case: null node contributes 0 sum and 0 count
41 if (node == null) {
42 return new int[]{0, 0};
43 }
44
45 // Recursively get sum and count from left subtree
46 int[] leftSubtree = calculateSubtreeInfo(node.left);
47
48 // Recursively get sum and count from right subtree
49 int[] rightSubtree = calculateSubtreeInfo(node.right);
50
51 // Calculate current subtree's sum: current node + left subtree + right subtree
52 int subtreeSum = node.val + leftSubtree[0] + rightSubtree[0];
53
54 // Calculate current subtree's node count: 1 (current) + left count + right count
55 int nodeCount = 1 + leftSubtree[1] + rightSubtree[1];
56
57 // Calculate average for current subtree and update maximum if needed
58 double currentAverage = (double) subtreeSum / nodeCount;
59 maxAverage = Math.max(maxAverage, currentAverage);
60
61 // Return sum and count for parent node's calculation
62 return new int[]{subtreeSum, nodeCount};
63 }
64}
65
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 * int val;
5 * TreeNode *left;
6 * TreeNode *right;
7 * TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14 double maximumAverageSubtree(TreeNode* root) {
15 double maxAverage = 0.0;
16
17 // Post-order traversal to calculate sum and count of nodes for each subtree
18 // Returns a pair of {sum of subtree values, number of nodes in subtree}
19 function<pair<int, int>(TreeNode*)> calculateSubtreeInfo = [&](TreeNode* node) -> pair<int, int> {
20 // Base case: empty node contributes 0 sum and 0 nodes
21 if (!node) {
22 return {0, 0};
23 }
24
25 // Recursively get sum and count from left subtree
26 auto [leftSum, leftCount] = calculateSubtreeInfo(node->left);
27
28 // Recursively get sum and count from right subtree
29 auto [rightSum, rightCount] = calculateSubtreeInfo(node->right);
30
31 // Calculate current subtree's total sum (current node + left + right)
32 int currentSum = node->val + leftSum + rightSum;
33
34 // Calculate current subtree's total node count (1 for current + left + right)
35 int currentCount = 1 + leftCount + rightCount;
36
37 // Calculate average for current subtree and update maximum if needed
38 double currentAverage = static_cast<double>(currentSum) / currentCount;
39 maxAverage = max(maxAverage, currentAverage);
40
41 // Return sum and count for parent's calculation
42 return {currentSum, currentCount};
43 };
44
45 // Start the DFS traversal from root
46 calculateSubtreeInfo(root);
47
48 return maxAverage;
49 }
50};
51
1/**
2 * Definition for a binary tree node.
3 */
4class TreeNode {
5 val: number;
6 left: TreeNode | null;
7 right: TreeNode | null;
8 constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
9 this.val = (val === undefined ? 0 : val);
10 this.left = (left === undefined ? null : left);
11 this.right = (right === undefined ? null : right);
12 }
13}
14
15/**
16 * Finds the maximum average value among all subtrees in a binary tree
17 * @param root - The root node of the binary tree
18 * @returns The maximum average value found across all subtrees
19 */
20function maximumAverageSubtree(root: TreeNode | null): number {
21 // Variable to track the maximum average found so far
22 let maxAverage: number = 0.0;
23
24 /**
25 * Helper function to calculate subtree information using post-order traversal
26 * @param node - Current node being processed
27 * @returns A tuple containing [sum of subtree values, number of nodes in subtree]
28 */
29 const calculateSubtreeInfo = (node: TreeNode | null): [number, number] => {
30 // Base case: null node contributes 0 sum and 0 nodes
31 if (!node) {
32 return [0, 0];
33 }
34
35 // Recursively calculate sum and count for left subtree
36 const [leftSum, leftCount] = calculateSubtreeInfo(node.left);
37
38 // Recursively calculate sum and count for right subtree
39 const [rightSum, rightCount] = calculateSubtreeInfo(node.right);
40
41 // Calculate total sum for current subtree (current node value + left subtree + right subtree)
42 const currentSum: number = node.val + leftSum + rightSum;
43
44 // Calculate total node count for current subtree (1 for current node + left count + right count)
45 const currentCount: number = 1 + leftCount + rightCount;
46
47 // Calculate average for current subtree and update maximum if this average is larger
48 const currentAverage: number = currentSum / currentCount;
49 maxAverage = Math.max(maxAverage, currentAverage);
50
51 // Return sum and count for parent node's calculation
52 return [currentSum, currentCount];
53 };
54
55 // Initiate the depth-first search traversal starting from the root
56 calculateSubtreeInfo(root);
57
58 return maxAverage;
59}
60
Time and Space Complexity
Time Complexity: O(n)
The algorithm performs a depth-first search (DFS) traversal of the binary tree. Each node is visited exactly once during the traversal. At each node, the operations performed are:
- Recursive calls to left and right children
- Constant time arithmetic operations (addition for sum, increment for count)
- Constant time comparison for updating the maximum average
Since we visit each of the n
nodes exactly once and perform O(1)
operations at each node, the overall time complexity is O(n)
.
Space Complexity: O(n)
The space complexity comes from the recursive call stack used during the DFS traversal. In the worst case, the tree could be completely unbalanced (essentially a linked list), where the depth of recursion would be n
. This would require O(n)
space on the call stack.
In the best case of a perfectly balanced tree, the recursion depth would be O(log n)
, but since we consider worst-case complexity, the space complexity is O(n)
.
Additionally, the algorithm uses only a constant amount of extra space for variables (ans
, s
, n
, ls
, ln
, rs
, rn
), which doesn't affect the overall space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Division Instead of Float Division
A critical mistake is performing integer division when calculating the average, which truncates the decimal part and leads to incorrect results.
Incorrect Implementation:
# In Python 2 or with integer division current_average = subtree_sum // subtree_count # Wrong! Loses precision
Correct Implementation:
# Use float division to maintain precision
current_average = subtree_sum / subtree_count # Correct
# Or explicitly convert to float
current_average = float(subtree_sum) / float(subtree_count)
2. Not Handling Single Node Trees Properly
Forgetting that a single node (leaf) is also a valid subtree can cause incorrect initialization or comparison logic.
Incorrect Implementation:
def maximumAverageSubtree(self, root):
if not root:
return 0
max_average = float('-inf') # Could miss single negative node
# ... rest of code
Correct Implementation:
def maximumAverageSubtree(self, root):
max_average = 0.0 # Or float('-inf') if negative values are possible
def dfs(node):
if node is None:
return 0, 0
# ... handle all nodes including leaves
3. Forgetting the nonlocal
Declaration
Without declaring max_average
as nonlocal
, the code will either create a local variable or raise an UnboundLocalError when trying to modify it.
Incorrect Implementation:
def maximumAverageSubtree(self, root):
max_average = 0.0
def dfs(node):
# ... calculate current_average
max_average = max(max_average, current_average) # UnboundLocalError!
return subtree_sum, subtree_count
Correct Implementation:
def maximumAverageSubtree(self, root):
max_average = 0.0
def dfs(node):
nonlocal max_average # Essential declaration
# ... calculate current_average
max_average = max(max_average, current_average)
return subtree_sum, subtree_count
4. Incorrect Order of Operations
Calculating the average before having complete subtree information leads to wrong results.
Incorrect Implementation:
def dfs(node):
if node is None:
return 0, 0
# Wrong: Calculating average before getting subtree info
current_average = node.val / 1
max_average = max(max_average, current_average)
left_sum, left_count = dfs(node.left)
right_sum, right_count = dfs(node.right)
# Missing the actual subtree average calculation
Correct Implementation:
def dfs(node):
if node is None:
return 0, 0
# First, get complete subtree information
left_sum, left_count = dfs(node.left)
right_sum, right_count = dfs(node.right)
# Then calculate the complete subtree values
subtree_sum = node.val + left_sum + right_sum
subtree_count = 1 + left_count + right_count
# Finally, calculate and compare average
current_average = subtree_sum / subtree_count
nonlocal max_average
max_average = max(max_average, current_average)
return subtree_sum, subtree_count
5. Returning Wrong Values from DFS
The helper function must return both sum and count for parent calculations. Returning only the average breaks the recursive logic.
Incorrect Implementation:
def dfs(node):
# ... calculate everything
current_average = subtree_sum / subtree_count
return current_average # Wrong! Parent can't use this
Correct Implementation:
def dfs(node):
# ... calculate everything
current_average = subtree_sum / subtree_count
nonlocal max_average
max_average = max(max_average, current_average)
return subtree_sum, subtree_count # Return sum and count for parent
Which of these properties could exist for a graph but not a tree?
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!