LeetCode 1120 - Maximum Average Subtree
This problem asks us to find the maximum average value among all subtrees of a given binary tree. A subtree is defined as any node along with all of its descendants. The average of a subtree is the sum of its node values divided by the number of nodes in that subtree.
Difficulty: 🟡 Medium
Topics: Tree, Depth-First Search, Binary Tree
Solution
Problem Understanding
This problem asks us to find the maximum average value among all subtrees of a given binary tree. A subtree is defined as any node along with all of its descendants. The average of a subtree is the sum of its node values divided by the number of nodes in that subtree.
The input is the root node of a binary tree. Each node contains an integer value, and the tree may have up to 10,000 nodes. Node values range from 0 to 10^5. The expected output is a floating-point number representing the maximum average value among all possible subtrees. Answers are considered correct if they are within 10^-5 of the actual maximum average.
Important edge cases include trees with only one node, trees with all nodes having the same value, or highly unbalanced trees. The constraints ensure that every tree has at least one node and that the number of nodes is moderate enough to allow a depth-first traversal solution.
Approaches
The brute-force approach involves computing the sum and number of nodes for every possible subtree separately. For each node, we would recursively calculate the sum and count for its subtree, compute the average, and keep track of the maximum. While correct, this approach is inefficient because it recomputes subtree sums multiple times. For a tree with n nodes, this can lead to O(n^2) time complexity in the worst case, which is unacceptable for n = 10^4.
The optimal approach uses a bottom-up depth-first search (DFS). By performing a post-order traversal, we can calculate the sum and count of nodes for each subtree exactly once. At each node, we compute the subtree sum and size by combining the results from the left and right children, calculate the average, and update the global maximum if needed. This ensures every node is visited only once, yielding an O(n) time complexity.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n^2) | O(h) | Recursively compute sum and count for each subtree separately; recomputation is expensive |
| Optimal | O(n) | O(h) | DFS post-order traversal; compute sum and count once per node; h is tree height |
Algorithm Walkthrough
- Initialize a global variable to track the maximum average found so far.
- Define a recursive helper function that accepts a node and returns a tuple
(subtree_sum, subtree_count). - If the node is
None, return(0, 0)since an empty subtree contributes no sum or nodes. - Recursively call the helper function on the left and right children to obtain their sums and counts.
- Compute the total sum for the current subtree as the sum of the node's value plus left and right subtree sums.
- Compute the total count of nodes in the current subtree as
1 + left_count + right_count. - Calculate the average for the current subtree as
total_sum / total_count. - Update the global maximum average if the current average is larger.
- Return
(total_sum, total_count)to the parent call. - Finally, after traversing the entire tree, return the maximum average.
Why it works: This approach ensures that each subtree’s sum and size are computed exactly once. By propagating these values up from the leaves to the root, we avoid redundant calculations while correctly considering every subtree for the maximum average.
Python Solution
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
from typing import Optional
class Solution:
def maximumAverageSubtree(self, root: Optional[TreeNode]) -> float:
self.max_avg = float('-inf')
def dfs(node: Optional[TreeNode]) -> tuple[int, int]:
if not node:
return (0, 0)
left_sum, left_count = dfs(node.left)
right_sum, right_count = dfs(node.right)
total_sum = node.val + left_sum + right_sum
total_count = 1 + left_count + right_count
current_avg = total_sum / total_count
self.max_avg = max(self.max_avg, current_avg)
return (total_sum, total_count)
dfs(root)
return self.max_avg
The Python solution uses a recursive DFS. The helper dfs returns both the sum and count of the subtree rooted at each node. The maximum average is updated during the post-order traversal. Using a tuple avoids recomputation and keeps the code concise.
Go Solution
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func maximumAverageSubtree(root *TreeNode) float64 {
var maxAvg float64 = -1
var dfs func(node *TreeNode) (int, int)
dfs = func(node *TreeNode) (int, int) {
if node == nil {
return 0, 0
}
leftSum, leftCount := dfs(node.Left)
rightSum, rightCount := dfs(node.Right)
totalSum := node.Val + leftSum + rightSum
totalCount := 1 + leftCount + rightCount
currentAvg := float64(totalSum) / float64(totalCount)
if currentAvg > maxAvg {
maxAvg = currentAvg
}
return totalSum, totalCount
}
dfs(root)
return maxAvg
}
The Go solution mirrors the Python approach, using a recursive DFS function. In Go, care is taken to convert integers to float64 for average calculation. The nil check replaces Python’s None.
Worked Examples
Example 1: [5,6,1]
| Node | Left Sum | Left Count | Right Sum | Right Count | Total Sum | Total Count | Average | Max Avg |
|---|---|---|---|---|---|---|---|---|
| 6 | 0 | 0 | 0 | 0 | 6 | 1 | 6.0 | 6.0 |
| 1 | 0 | 0 | 0 | 0 | 1 | 1 | 1.0 | 6.0 |
| 5 | 6 | 1 | 1 | 1 | 12 | 3 | 4.0 | 6.0 |
Maximum average is 6.0.
Example 2: [0,null,1]
| Node | Left Sum | Left Count | Right Sum | Right Count | Total Sum | Total Count | Average | Max Avg |
|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 0 | 0 | 0 | 1 | 1 | 1.0 | 1.0 |
| 0 | 0 | 0 | 1 | 1 | 1 | 2 | 0.5 | 1.0 |
Maximum average is 1.0.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each node is visited exactly once in the DFS traversal |
| Space | O(h) | Recursion stack for a tree of height h; worst-case O(n) for skewed tree |
The algorithm is efficient because it avoids redundant calculations and traverses the tree only once. Space usage is proportional to the recursion depth.
Test Cases
# test cases
root1 = TreeNode(5, TreeNode(6), TreeNode(1))
assert abs(Solution().maximumAverageSubtree(root1) - 6.0) < 1e-5 # Example 1
root2 = TreeNode(0, None, TreeNode(1))
assert abs(Solution().maximumAverageSubtree(root2) - 1.0) < 1e-5 # Example 2
root3 = TreeNode(1)
assert abs(Solution().maximumAverageSubtree(root3) - 1.0) < 1e-5 # Single node
root4 = TreeNode(1, TreeNode(1), TreeNode(1))
assert abs(Solution().maximumAverageSubtree(root4) - 1.0) < 1e-5 # All same values
root5 = TreeNode(1, TreeNode(2, TreeNode(3, TreeNode(4))), None)
assert abs(Solution().maximumAverageSubtree(root5) - 4.0) < 1e-5 # Skewed tree
| Test | Why |
|---|---|
[5,6,1] |
Normal small tree with varied values |
[0,null,1] |
Tree with missing left child |
[1] |
Single-node tree |
[1,1,1] |
Uniform values |
Skewed [1,2,3,4] |
Deep left-skewed tree to check recursion |
Edge Cases
Single Node Tree: When the tree has only one node, the maximum average is simply the value of that node. The implementation handles this naturally because the DFS base case returns (0,0) for null children.
Uniform Values: If all nodes have the same value, any subtree has the same average. The maximum