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 value5
would be a uni-value subtree - A subtree with root value
5
, left child5
, and right child3
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:
- Whether the left subtree is uni-value
- Whether the right subtree is uni-value
- 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:
- We need to examine every subtree in the binary tree to count uni-value subtrees
- 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
- This bottom-up information gathering pattern is naturally handled by DFS with post-order traversal
- 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).
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:
- Is the left subtree (if it exists) uni-value?
- Is the right subtree (if it exists) uni-value?
- 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.
-
Base Case: If the current node is
None
, returnTrue
. An empty subtree is considered uni-value by definition. -
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).
-
Early Termination: If either subtree is not uni-value, the current subtree cannot be uni-value:
if not l or not r: return False
-
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.
-
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. -
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 EvaluatorExample 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, returningTrue
.- 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
- Left child value:
- Since
a == b == 5
, this is a uni-value subtree!ans = 1
, returnTrue
.
Step 3: Process middle-left leaf (value 5)
- Similar to Step 2, this leaf is also a uni-value subtree.
ans = 2
, returnTrue
.
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
- Left child value:
- Since
a == b == 5
, this entire left subtree is uni-value!ans = 3
, returnTrue
.
Step 5: Process rightmost leaf (value 5)
- The single leaf on the right side is uni-value.
ans = 4
, returnTrue
.
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
- Left child value:
- Since
a == b == 5
, this right subtree is uni-value!ans = 5
, returnTrue
.
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
- Left child value:
- Since
a == b == 5
, the entire tree is uni-value!ans = 6
, returnTrue
.
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 inO(log n)
space complexity - For a skewed tree (essentially a linked list), the height
h = O(n)
, resulting inO(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
What's the relationship between a tree and a graph?
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!