Facebook Pixel

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:

  1. The sum of all values in the subtree rooted at that node
  2. The count of nodes in that subtree
  3. 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:

  1. Traverse the entire tree to examine all possible subtrees
  2. Aggregate information (sum and count) from child nodes before processing parent nodes
  3. Calculate averages for each subtree during the traversal
  4. Track the maximum average found across all subtrees
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. We recursively get (sum, count) from the left child
  2. We recursively get (sum, count) from the right child
  3. We calculate the current subtree's total sum: current_sum = node.val + left_sum + right_sum
  4. We calculate the current subtree's total count: current_count = 1 + left_count + right_count
  5. We compute the average: average = current_sum / current_count
  6. 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:

  1. 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
  2. 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
    • 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
    • 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

Key Design Choices:

  • Using nonlocal: The ans variable is declared as nonlocal 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) where n is the number of nodes, as we visit each node exactly once
  • Space: O(h) where h 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 Evaluator

Example 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):

  1. Start at root (5), but first we need to process its children.

  2. Go to left child (6), but first process its children.

  3. 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)
  4. 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)
  5. 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)
  6. 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)

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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of these properties could exist for a graph but not a tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More