Facebook Pixel

250. Count Univalue Subtrees 🔒

Problem Description

You are given the root of a binary tree. Your task is to count how many uni-value subtrees exist in the tree.

A uni-value subtree is a subtree where all nodes have the same value. This means that starting from any node in the tree, if you consider it as the root of a subtree, all nodes within that subtree (including the node itself, its children, grandchildren, and so on) must have identical values for it to qualify as a uni-value subtree.

For example:

  • A single leaf node is always a uni-value subtree (since it has no children)
  • A subtree with root value 5 and two children both with value 5 would be a uni-value subtree
  • A subtree with root value 5, left child 5, and right child 3 would NOT be a uni-value subtree

The solution uses a depth-first search (DFS) approach that traverses the tree from bottom to top. For each node, it checks:

  1. Whether the left subtree is uni-value
  2. Whether the right subtree is uni-value
  3. Whether the current node's value matches its children's values (if they exist)

The function returns True if the current subtree is uni-value and False otherwise. When a uni-value subtree is found, the counter ans is incremented. The clever part is handling null children - if a child doesn't exist, the code treats it as having the same value as the parent node (a = root.val if root.left is None else root.left.val).

The algorithm counts all uni-value subtrees by checking each node and its descendants, ultimately returning the total count.

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 where each node has at most two children).

Is it a tree?

  • Yes: The problem explicitly states we're working with a binary tree structure, where we need to examine subtrees and their properties.

DFS

  • Following the flowchart, when we have a tree structure, the natural choice is DFS (Depth-First Search).

Conclusion: The flowchart suggests using a DFS approach for this problem.

This makes perfect sense because:

  1. We need to examine every subtree in the binary tree to count uni-value subtrees
  2. For each node, we need to know if its left and right subtrees are uni-value before determining if the current subtree is uni-value
  3. This bottom-up information gathering pattern is naturally handled by DFS with post-order traversal
  4. DFS allows us to recursively process children before the parent, which is essential for determining if a subtree rooted at any node has uniform values

The DFS pattern in the solution recursively traverses to leaf nodes first, then works its way back up, checking at each node whether the subtree rooted at that node is uni-value by comparing the node's value with its children's values (if they exist).

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that to determine if a subtree is uni-value, we need information from its children first. This naturally leads us to think about a bottom-up approach.

Consider a simple example: if we have a node with value 5, how do we know if the subtree rooted at this node is uni-value? We need to check:

  1. Is the left subtree (if it exists) uni-value?
  2. Is the right subtree (if it exists) uni-value?
  3. Do all nodes in both subtrees have the same value as the current node?

This dependency pattern suggests we should process leaf nodes first (they're trivially uni-value), then work our way up to parent nodes. This is exactly what post-order DFS gives us.

The clever observation is that we don't need to track all values in a subtree. Instead, we only need to know:

  • Whether a subtree is uni-value (boolean)
  • The value at the root of that subtree (which represents all values if it's uni-value)

When we're at a node, if both its subtrees are uni-value AND their root values match the current node's value, then the entire subtree rooted at the current node is also uni-value.

The solution elegantly handles edge cases by treating non-existent children as having the same value as their parent (a = root.val if root.left is None else root.left.val). This makes sense because a null child doesn't break the uni-value property.

By using a counter that increments each time we find a uni-value subtree and returning a boolean to indicate whether the current subtree is uni-value, we can count all uni-value subtrees in a single tree traversal with O(n) time complexity.

Solution Approach

The solution implements a post-order DFS traversal to count uni-value subtrees. Here's how the implementation works:

Core DFS Function:

The dfs function returns a boolean indicating whether the subtree rooted at the current node is uni-value.

  1. Base Case: If the current node is None, return True. An empty subtree is considered uni-value by definition.

  2. Recursive Calls: First, recursively check the left and right subtrees:

    l, r = dfs(root.left), dfs(root.right)

    These calls will process all descendants before the current node (post-order traversal).

  3. Early Termination: If either subtree is not uni-value, the current subtree cannot be uni-value:

    if not l or not r:
        return False
  4. Value Extraction: Get the values to compare. If a child doesn't exist, use the parent's value:

    a = root.val if root.left is None else root.left.val
    b = root.val if root.right is None else root.right.val

    This clever trick handles null children gracefully - a missing child doesn't violate the uni-value property.

  5. Uni-value Check: If the current node's value matches both children's values (or the node's own value for null children), the subtree is uni-value:

    if a == b == root.val:
        nonlocal ans
        ans += 1
        return True

    When we find a uni-value subtree, increment the counter and return True to inform the parent.

  6. Return False: If values don't match, this subtree is not uni-value.

Counter Management:

The solution uses a nonlocal variable ans to maintain the count across all recursive calls. This avoids the need to pass and return the count through the recursion stack.

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, for the recursion stack

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to illustrate how the solution works.

Consider this binary tree:

        5
       / \
      5   5
     / \   \
    5   5   5

We'll trace through the DFS traversal to see how uni-value subtrees are counted.

Step 1: Start DFS from root (value 5)

  • The function begins at the root and immediately makes recursive calls to process the left and right subtrees first (post-order traversal).

Step 2: Process leftmost leaf (value 5)

  • We reach the leftmost leaf node (bottom-left 5).
  • dfs(None) is called for both its children, returning True.
  • Since both children are "uni-value" (they're null), we check values:
    • Left child value: a = 5 (uses parent's value since left child is None)
    • Right child value: b = 5 (uses parent's value since right child is None)
    • Current node value: 5
  • Since a == b == 5, this is a uni-value subtree! ans = 1, return True.

Step 3: Process middle-left leaf (value 5)

  • Similar to Step 2, this leaf is also a uni-value subtree.
  • ans = 2, return True.

Step 4: Process left subtree root (value 5)

  • Now we're back at the left child of the main root.
  • Both its children returned True (they're uni-value).
  • Check values:
    • Left child value: a = 5 (from actual left child)
    • Right child value: b = 5 (from actual right child)
    • Current node value: 5
  • Since a == b == 5, this entire left subtree is uni-value! ans = 3, return True.

Step 5: Process rightmost leaf (value 5)

  • The single leaf on the right side is uni-value.
  • ans = 4, return True.

Step 6: Process right subtree root (value 5)

  • Check values:
    • Left child value: a = 5 (uses parent's value since left child is None)
    • Right child value: b = 5 (from actual right child)
    • Current node value: 5
  • Since a == b == 5, this right subtree is uni-value! ans = 5, return True.

Step 7: Process main root (value 5)

  • Both subtrees returned True.
  • Check values:
    • Left child value: a = 5 (from left subtree root)
    • Right child value: b = 5 (from right subtree root)
    • Current node value: 5
  • Since a == b == 5, the entire tree is uni-value! ans = 6, return True.

Final Result: 6 uni-value subtrees

The algorithm correctly identified all 6 uni-value subtrees: 3 leaf nodes, 2 intermediate subtrees, and the entire tree itself.

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 countUnivalSubtrees(self, root: Optional[TreeNode]) -> int:
12        """
13        Count the number of uni-value subtrees in a binary tree.
14        A uni-value subtree means all nodes of the subtree have the same value.
15      
16        Args:
17            root: Root node of the binary tree
18          
19        Returns:
20            Number of uni-value subtrees
21        """
22        # Initialize counter for uni-value subtrees
23        self.univalue_count = 0
24      
25        def is_univalue_subtree(node: Optional[TreeNode]) -> bool:
26            """
27            Helper function to check if subtree rooted at node is uni-value.
28          
29            Args:
30                node: Current node being processed
31              
32            Returns:
33                True if subtree rooted at node is uni-value, False otherwise
34            """
35            # Base case: empty tree is considered uni-value
36            if node is None:
37                return True
38          
39            # Recursively check left and right subtrees
40            left_is_univalue = is_univalue_subtree(node.left)
41            right_is_univalue = is_univalue_subtree(node.right)
42          
43            # If either subtree is not uni-value, current subtree cannot be uni-value
44            if not left_is_univalue or not right_is_univalue:
45                return False
46          
47            # Get values to compare: use current node's value if child is None
48            left_value = node.val if node.left is None else node.left.val
49            right_value = node.val if node.right is None else node.right.val
50          
51            # Check if current node forms a uni-value subtree with its children
52            if left_value == right_value == node.val:
53                # Increment counter for valid uni-value subtree
54                self.univalue_count += 1
55                return True
56          
57            # Current subtree is not uni-value
58            return False
59      
60        # Start DFS traversal from root
61        is_univalue_subtree(root)
62      
63        return self.univalue_count
64
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    // Counter to track the total number of uni-value subtrees
18    private int uniValueSubtreeCount;
19
20    /**
21     * Counts the number of uni-value subtrees in a binary tree.
22     * A uni-value subtree means all nodes of the subtree have the same value.
23     * 
24     * @param root The root of the binary tree
25     * @return The total count of uni-value subtrees
26     */
27    public int countUnivalSubtrees(TreeNode root) {
28        uniValueSubtreeCount = 0;
29        isUniValueSubtree(root);
30        return uniValueSubtreeCount;
31    }
32
33    /**
34     * Performs a post-order traversal to check if the subtree rooted at the given node
35     * is a uni-value subtree.
36     * 
37     * @param node The current node being processed
38     * @return true if the subtree rooted at this node is a uni-value subtree, false otherwise
39     */
40    private boolean isUniValueSubtree(TreeNode node) {
41        // Base case: an empty tree is considered a uni-value subtree
42        if (node == null) {
43            return true;
44        }
45      
46        // Recursively check if left and right subtrees are uni-value subtrees
47        boolean isLeftUniValue = isUniValueSubtree(node.left);
48        boolean isRightUniValue = isUniValueSubtree(node.right);
49      
50        // If either subtree is not a uni-value subtree, current subtree cannot be uni-value
51        if (!isLeftUniValue || !isRightUniValue) {
52            return false;
53        }
54      
55        // Get the value from left child (or use current node's value if left child is null)
56        int leftValue = (node.left == null) ? node.val : node.left.val;
57      
58        // Get the value from right child (or use current node's value if right child is null)
59        int rightValue = (node.right == null) ? node.val : node.right.val;
60      
61        // Check if current node, left child, and right child all have the same value
62        if (leftValue == rightValue && rightValue == node.val) {
63            // Increment counter as we found a uni-value subtree
64            uniValueSubtreeCount++;
65            return true;
66        }
67      
68        // Current subtree is not a uni-value subtree
69        return false;
70    }
71}
72
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    int countUnivalSubtrees(TreeNode* root) {
15        int count = 0;
16      
17        // Helper function to check if subtree is unival and count unival subtrees
18        // Returns true if the subtree rooted at 'node' is a unival subtree
19        function<bool(TreeNode*)> isUnivalSubtree = [&](TreeNode* node) -> bool {
20            // Base case: empty tree is considered unival
21            if (!node) {
22                return true;
23            }
24          
25            // Recursively check left and right subtrees
26            bool isLeftUnival = isUnivalSubtree(node->left);
27            bool isRightUnival = isUnivalSubtree(node->right);
28          
29            // If either subtree is not unival, current subtree cannot be unival
30            if (!isLeftUnival || !isRightUnival) {
31                return false;
32            }
33          
34            // Get values from left and right children
35            // If child doesn't exist, use current node's value for comparison
36            int leftValue = node->left ? node->left->val : node->val;
37            int rightValue = node->right ? node->right->val : node->val;
38          
39            // Check if current node forms a unival subtree with its children
40            if (leftValue == rightValue && rightValue == node->val) {
41                // All values are same, increment count and return true
42                ++count;
43                return true;
44            }
45          
46            // Values don't match, not a unival subtree
47            return false;
48        };
49      
50        // Start DFS traversal from root
51        isUnivalSubtree(root);
52      
53        return count;
54    }
55};
56
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 *     val: number
5 *     left: TreeNode | null
6 *     right: TreeNode | null
7 *     constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 *         this.val = (val===undefined ? 0 : val)
9 *         this.left = (left===undefined ? null : left)
10 *         this.right = (right===undefined ? null : right)
11 *     }
12 * }
13 */
14
15/**
16 * Counts the number of uni-value subtrees in a binary tree.
17 * A uni-value subtree means all nodes of the subtree have the same value.
18 * 
19 * @param root - The root node of the binary tree
20 * @returns The total count of uni-value subtrees
21 */
22function countUnivalSubtrees(root: TreeNode | null): number {
23    // Counter for tracking the number of uni-value subtrees
24    let uniValueSubtreeCount: number = 0;
25  
26    /**
27     * Performs depth-first search to check if the subtree rooted at the given node is uni-value.
28     * 
29     * @param node - The current node being processed
30     * @returns true if the subtree rooted at this node is uni-value, false otherwise
31     */
32    const isUniValueSubtree = (node: TreeNode | null): boolean => {
33        // Base case: empty tree is considered uni-value
34        if (node === null) {
35            return true;
36        }
37      
38        // Recursively check left and right subtrees
39        const isLeftUniValue: boolean = isUniValueSubtree(node.left);
40        const isRightUniValue: boolean = isUniValueSubtree(node.right);
41      
42        // If either subtree is not uni-value, current subtree cannot be uni-value
43        if (!isLeftUniValue || !isRightUniValue) {
44            return false;
45        }
46      
47        // Check if left child exists and has different value than current node
48        if (node.left !== null && node.left.val !== node.val) {
49            return false;
50        }
51      
52        // Check if right child exists and has different value than current node
53        if (node.right !== null && node.right.val !== node.val) {
54            return false;
55        }
56      
57        // Current subtree is uni-value, increment counter
58        uniValueSubtreeCount++;
59        return true;
60    };
61  
62    // Start DFS traversal from root
63    isUniValueSubtree(root);
64  
65    return uniValueSubtreeCount;
66}
67

Time and Space Complexity

Time Complexity: O(n) where n is the number of nodes in the binary tree.

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 algorithm performs constant time operations:

  • Checking if the node is None
  • Comparing values (a == b == root.val)
  • Incrementing the counter
  • Returning a boolean value

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(h) where h is the height of the binary tree.

The space complexity is determined by the recursive call stack. In the worst case, the recursion depth equals the height of the tree:

  • For a balanced binary tree, the height h = O(log n), resulting in O(log n) space complexity
  • For a skewed tree (essentially a linked list), the height h = O(n), resulting in O(n) space complexity

The algorithm uses a constant amount of extra space for variables (ans, a, b, l, r) at each recursive call level, which doesn't change the overall space complexity.

Therefore, the space complexity is O(h) in general, which ranges from O(log n) in the best case (balanced tree) to O(n) in the worst case (skewed tree).

Common Pitfalls

1. Incorrect Handling of Null Children

Pitfall: A common mistake is to check if children exist and their values match the parent separately, leading to verbose and error-prone code:

# Incorrect approach - overly complex
if node.left and node.left.val != node.val:
    return False
if node.right and node.right.val != node.val:
    return False

This approach requires multiple conditional checks and can miss edge cases.

Solution: Use the elegant trick of assigning the parent's value when a child is null:

left_value = node.val if node.left is None else node.left.val
right_value = node.val if node.right is None else node.right.val

2. Forgetting to Check Subtree Validity Before Value Comparison

Pitfall: Checking only if the current node's value matches its children without first verifying that the children's subtrees are uni-value:

# Wrong - doesn't check if subtrees are uni-value first
def is_univalue_subtree(node):
    if node is None:
        return True
  
    # Incorrectly checking values before subtree validity
    if node.left and node.left.val != node.val:
        return False
    if node.right and node.right.val != node.val:
        return False
  
    # Now checking subtrees - wrong order!
    left_is_univalue = is_univalue_subtree(node.left)
    right_is_univalue = is_univalue_subtree(node.right)
  
    if left_is_univalue and right_is_univalue:
        self.count += 1
        return True
    return False

Solution: Always check subtree validity first with recursive calls, then verify value equality:

# First recurse to check subtrees
left_is_univalue = is_univalue_subtree(node.left)
right_is_univalue = is_univalue_subtree(node.right)

# Early return if subtrees aren't uni-value
if not left_is_univalue or not right_is_univalue:
    return False

# Then check values

3. Not Counting All Valid Subtrees

Pitfall: Only counting when returning True but forgetting to count leaf nodes or missing certain valid subtrees:

# Wrong - might miss counting some valid subtrees
def is_univalue_subtree(node):
    if node is None:
        return True
  
    if node.left is None and node.right is None:
        # Forgot to increment counter for leaf nodes!
        return True

Solution: Ensure the counter is incremented whenever a uni-value subtree is identified, including single nodes:

if left_value == right_value == node.val:
    self.univalue_count += 1  # Count this uni-value subtree
    return True

4. Using Global Variables Instead of Class Attributes

Pitfall: Using nonlocal with nested functions can be confusing and less maintainable:

def countUnivalSubtrees(self, root):
    ans = 0  # Local variable
  
    def dfs(node):
        nonlocal ans  # Easy to forget this line
        # ... rest of code

Solution: Use a class attribute for cleaner code:

def countUnivalSubtrees(self, root):
    self.univalue_count = 0  # Class attribute
  
    def is_univalue_subtree(node):
        # No need for nonlocal declaration
        # ... 
        self.univalue_count += 1

5. Incorrect Base Case Handling

Pitfall: Returning the wrong value for null nodes or not handling them properly:

# Wrong - null nodes shouldn't increment counter
def is_univalue_subtree(node):
    if node is None:
        self.univalue_count += 1  # Wrong!
        return True

Solution: Null nodes should return True (they don't violate uni-value property) but shouldn't increment the counter:

if node is None:
    return True  # Just return True, don't count
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What's the relationship between a tree and a graph?


Recommended Readings

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

Load More