3319. K-th Largest Perfect Subtree Size in Binary Tree
Problem Description
You are given the root of a binary tree and an integer k
. Your task is to find the size of the k
-th largest perfect binary subtree in the given tree. If there are fewer than k
perfect binary subtrees in the tree, return -1
.
A perfect binary tree is defined as a tree where:
- All leaf nodes are at the same level (same depth from the root)
- Every parent node has exactly two children
The problem asks you to:
- Identify all perfect binary subtrees within the given binary tree
- Determine the size of each perfect binary subtree (size = total number of nodes in that subtree)
- Find the
k
-th largest among these sizes - Return this
k
-th largest size, or-1
if fewer thank
perfect binary subtrees exist
For example, a single node is considered a perfect binary tree of size 1. A tree with a root and two leaf children forms a perfect binary tree of size 3. The algorithm needs to examine every possible subtree in the given tree to identify which ones are perfect binary trees, calculate their sizes, and then return the k
-th largest size after sorting them in descending order.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: A binary tree is a special type of graph where each node has at most two children and there are no cycles.
Is it a tree?
- Yes: The problem explicitly states we are working with a binary tree structure, where we have a root node and each node can have left and right children.
DFS
- Conclusion: The flowchart leads us directly to DFS (Depth-First Search) for tree problems.
This makes perfect sense for our problem because:
- We need to traverse the entire tree to identify all perfect binary subtrees
- For each node, we need to check if the subtree rooted at that node forms a perfect binary tree
- DFS allows us to process each subtree from the bottom up, calculating the size of perfect subtrees recursively
- At each node, we can determine if it's the root of a perfect binary subtree by checking if its left and right subtrees are perfect and have equal sizes
The DFS approach naturally fits this problem as we need to:
- Visit each node in the tree
- Compute properties (perfection and size) of subtrees before processing their parent
- Aggregate information from child nodes to parent nodes
Conclusion: The flowchart correctly identifies DFS as the optimal approach for this tree-based problem where we need to analyze properties of all subtrees.
Intuition
The key insight is that we need to identify and measure all perfect binary subtrees within the given tree. A perfect binary tree has a very specific property: all leaves are at the same depth, and every internal node has exactly two children. This means that for any perfect binary tree, the left and right subtrees must also be perfect binary trees of the exact same size.
This observation leads us to a bottom-up approach. We can start from the leaves and work our way up to the root. A leaf node by itself is a perfect binary tree of size 1. For any internal node to be the root of a perfect binary tree, both its left and right children must exist and must be roots of perfect binary trees of equal size.
Think about how we can determine if a subtree is perfect:
- If we're at a null node, we can consider it as having size 0
- If we're at a leaf node (both children are null), it's a perfect tree of size 1
- For any other node, we need to check if both left and right subtrees are perfect and have the same size
This naturally suggests a recursive DFS approach where we:
- Recursively calculate the perfect subtree size for left and right children
- If both children return the same positive size (or 0 for null nodes), then the current subtree is perfect with size
left_size + right_size + 1
- If the children have different sizes or one of them is not perfect (returns -1), then the current subtree is not perfect
As we traverse the tree, we can collect all the sizes of perfect subtrees we find. Once we have all sizes, finding the k
-th largest is simply a matter of sorting these sizes in descending order and picking the k
-th element.
The use of -1 as a sentinel value is clever here - it propagates upward to indicate that a subtree is not perfect, preventing any parent node from being incorrectly identified as perfect.
Learn more about Tree, Depth-First Search, Binary Tree and Sorting patterns.
Solution Approach
The solution implements a DFS traversal with post-order processing to identify and measure all perfect binary subtrees. Here's how the implementation works:
Core DFS Function (dfs
)
The recursive dfs
function calculates the size of a perfect binary subtree rooted at the current node:
-
Base Case: If the current node is
None
, return0
(an empty tree has size 0) -
Recursive Calls: Calculate the sizes of left and right subtrees:
l, r = dfs(root.left), dfs(root.right)
-
Perfect Subtree Check: A subtree is perfect only if:
- Both left and right subtrees are perfect (neither returns
-1
) - Both subtrees have the same size (
l == r
)
If either condition fails, return
-1
to indicate this subtree is not perfect:if l < 0 or l != r: return -1
- Both left and right subtrees are perfect (neither returns
-
Size Calculation: If the subtree is perfect, calculate its total size:
cnt = l + r + 1 # left subtree + right subtree + current node
-
Store Perfect Subtree Size: Add the size to our collection:
nums.append(cnt)
-
Return Size: Return
cnt
so parent nodes can use this information
Main Algorithm Flow
-
Initialize Storage: Create an empty list
nums
to store all perfect subtree sizes -
Traverse Tree: Call
dfs(root)
to process the entire tree and populatenums
-
Check Availability: If we have fewer than
k
perfect subtrees, return-1
:if len(nums) < k: return -1
-
Find k-th Largest: Sort the sizes in descending order and return the k-th element:
nums.sort(reverse=True) return nums[k - 1] # k-1 because of 0-based indexing
Time Complexity: O(n + m log m)
where n
is the number of nodes in the tree and m
is the number of perfect subtrees. We visit each node once (O(n)
) and sort the perfect subtree sizes (O(m log m)
).
Space Complexity: O(n)
for the recursion stack in the worst case (skewed tree) plus O(m)
for storing perfect subtree sizes.
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 algorithm works.
Consider this binary tree and let's find the 2nd largest perfect binary subtree (k=2):
1 / \ 2 3 / \ 4 5
Step-by-step DFS traversal (post-order):
-
Visit node 4 (leaf):
- Left child:
dfs(None)
returns 0 - Right child:
dfs(None)
returns 0 - Check:
l = 0, r = 0
, they're equal and non-negative - Size:
0 + 0 + 1 = 1
- Add 1 to nums:
nums = [1]
- Return 1
- Left child:
-
Visit node 5 (leaf):
- Left child:
dfs(None)
returns 0 - Right child:
dfs(None)
returns 0 - Check:
l = 0, r = 0
, they're equal and non-negative - Size:
0 + 0 + 1 = 1
- Add 1 to nums:
nums = [1, 1]
- Return 1
- Left child:
-
Visit node 2:
- Left child:
dfs(4)
returns 1 - Right child:
dfs(5)
returns 1 - Check:
l = 1, r = 1
, they're equal and non-negative - Size:
1 + 1 + 1 = 3
- Add 3 to nums:
nums = [1, 1, 3]
- Return 3
- Left child:
-
Visit node 3 (leaf):
- Left child:
dfs(None)
returns 0 - Right child:
dfs(None)
returns 0 - Check:
l = 0, r = 0
, they're equal and non-negative - Size:
0 + 0 + 1 = 1
- Add 1 to nums:
nums = [1, 1, 3, 1]
- Return 1
- Left child:
-
Visit node 1 (root):
- Left child:
dfs(2)
returns 3 - Right child:
dfs(3)
returns 1 - Check:
l = 3, r = 1
, they're NOT equal - This is NOT a perfect binary tree
- Return -1
- Left child:
Final processing:
- All perfect subtree sizes:
nums = [1, 1, 3, 1]
- Sort in descending order:
[3, 1, 1, 1]
- We need the 2nd largest (k=2):
nums[1] = 1
- Return 1
The algorithm correctly identifies:
- Three leaf nodes as perfect trees of size 1
- The subtree rooted at node 2 as a perfect tree of size 3
- The entire tree is NOT perfect (different subtree sizes)
The 2nd largest perfect binary subtree has size 1.
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 kthLargestPerfectSubtree(self, root: Optional[TreeNode], k: int) -> int:
12 """
13 Find the kth largest perfect subtree size in a binary tree.
14 A perfect binary tree is one where all internal nodes have exactly 2 children
15 and all leaves are at the same level.
16
17 Args:
18 root: Root node of the binary tree
19 k: The k-th largest value to find
20
21 Returns:
22 Size of the k-th largest perfect subtree, or -1 if fewer than k perfect subtrees exist
23 """
24
25 def dfs(node: Optional[TreeNode]) -> int:
26 """
27 Depth-first search to find perfect subtrees.
28
29 Args:
30 node: Current node being processed
31
32 Returns:
33 Size of perfect subtree rooted at node if it exists, -1 otherwise
34 """
35 # Base case: empty node represents a perfect subtree of size 0
36 if node is None:
37 return 0
38
39 # Recursively check left and right subtrees
40 left_size = dfs(node.left)
41 right_size = dfs(node.right)
42
43 # Check if current subtree is perfect:
44 # 1. Both subtrees must be perfect (not -1)
45 # 2. Both subtrees must have equal size (same height)
46 if left_size < 0 or left_size != right_size:
47 return -1
48
49 # Calculate size of current perfect subtree
50 current_tree_size = left_size + right_size + 1
51
52 # Store the size of this perfect subtree
53 perfect_subtree_sizes.append(current_tree_size)
54
55 return current_tree_size
56
57 # List to store sizes of all perfect subtrees found
58 perfect_subtree_sizes = []
59
60 # Traverse the tree to find all perfect subtrees
61 dfs(root)
62
63 # Check if we have at least k perfect subtrees
64 if len(perfect_subtree_sizes) < k:
65 return -1
66
67 # Sort sizes in descending order to find k-th largest
68 perfect_subtree_sizes.sort(reverse=True)
69
70 # Return k-th largest (k-1 index due to 0-based indexing)
71 return perfect_subtree_sizes[k - 1]
72
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 // List to store sizes of all perfect subtrees found
18 private List<Integer> perfectSubtreeSizes = new ArrayList<>();
19
20 /**
21 * Finds the k-th largest perfect subtree in the binary tree.
22 * A perfect binary tree is one where all internal nodes have exactly 2 children
23 * and all leaves are at the same level.
24 *
25 * @param root The root of the binary tree
26 * @param k The k-th largest size to find (1-indexed)
27 * @return The size of the k-th largest perfect subtree, or -1 if less than k perfect subtrees exist
28 */
29 public int kthLargestPerfectSubtree(TreeNode root, int k) {
30 // Traverse the tree and collect all perfect subtree sizes
31 findPerfectSubtrees(root);
32
33 // Check if we have at least k perfect subtrees
34 if (perfectSubtreeSizes.size() < k) {
35 return -1;
36 }
37
38 // Sort sizes in descending order
39 perfectSubtreeSizes.sort(Comparator.reverseOrder());
40
41 // Return the k-th largest (k-1 index since list is 0-indexed)
42 return perfectSubtreeSizes.get(k - 1);
43 }
44
45 /**
46 * Recursively traverses the tree to find perfect subtrees.
47 *
48 * @param node The current node being processed
49 * @return The size of the perfect subtree rooted at this node,
50 * or -1 if the subtree is not perfect
51 */
52 private int findPerfectSubtrees(TreeNode node) {
53 // Base case: null node represents an empty perfect subtree of size 0
54 if (node == null) {
55 return 0;
56 }
57
58 // Recursively check left and right subtrees
59 int leftSize = findPerfectSubtrees(node.left);
60 int rightSize = findPerfectSubtrees(node.right);
61
62 // Check if current subtree is perfect:
63 // 1. Both children must be perfect (not -1)
64 // 2. Both children must have the same size (balanced)
65 if (leftSize < 0 || leftSize != rightSize) {
66 return -1; // Current subtree is not perfect
67 }
68
69 // Calculate total size of this perfect subtree
70 // (left subtree + right subtree + current node)
71 int currentSubtreeSize = leftSize + rightSize + 1;
72
73 // Add this perfect subtree size to our collection
74 perfectSubtreeSizes.add(currentSubtreeSize);
75
76 // Return the size for parent node's calculation
77 return currentSubtreeSize;
78 }
79}
80
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 kthLargestPerfectSubtree(TreeNode* root, int k) {
15 // Store sizes of all perfect binary subtrees
16 vector<int> perfectSubtreeSizes;
17
18 // Define recursive DFS function to find perfect binary subtrees
19 // Returns the size of perfect subtree rooted at current node, or -1 if not perfect
20 auto dfs = [&](this auto&& dfs, TreeNode* node) -> int {
21 // Base case: empty node is considered a perfect subtree of size 0
22 if (!node) {
23 return 0;
24 }
25
26 // Recursively get sizes of left and right subtrees
27 int leftSize = dfs(node->left);
28 int rightSize = dfs(node->right);
29
30 // If either subtree is not perfect OR sizes don't match,
31 // current subtree is not perfect
32 if (leftSize < 0 || leftSize != rightSize) {
33 return -1;
34 }
35
36 // Calculate total size of current perfect subtree (left + right + root)
37 int currentSubtreeSize = leftSize + rightSize + 1;
38
39 // Add this perfect subtree size to our collection
40 perfectSubtreeSizes.push_back(currentSubtreeSize);
41
42 // Return size for parent's calculation
43 return currentSubtreeSize;
44 };
45
46 // Perform DFS traversal to find all perfect subtrees
47 dfs(root);
48
49 // Check if we have at least k perfect subtrees
50 if (perfectSubtreeSizes.size() < k) {
51 return -1;
52 }
53
54 // Sort sizes in descending order to find kth largest
55 ranges::sort(perfectSubtreeSizes, greater<int>());
56
57 // Return the kth largest perfect subtree size (k-1 due to 0-indexing)
58 return perfectSubtreeSizes[k - 1];
59 }
60};
61
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 * Finds the kth largest perfect subtree in a binary tree
17 * A perfect binary tree is one where all internal nodes have exactly two children
18 * and all leaf nodes are at the same level
19 *
20 * @param root - The root node of the binary tree
21 * @param k - The k-th largest perfect subtree to find (1-indexed)
22 * @returns The size of the k-th largest perfect subtree, or -1 if fewer than k perfect subtrees exist
23 */
24function kthLargestPerfectSubtree(root: TreeNode | null, k: number): number {
25 // Array to store sizes of all perfect subtrees found
26 const perfectSubtreeSizes: number[] = [];
27
28 /**
29 * DFS helper function to check if a subtree is perfect and return its size
30 *
31 * @param node - Current node being processed
32 * @returns The number of nodes in the perfect subtree rooted at this node,
33 * or -1 if the subtree is not perfect
34 */
35 const checkPerfectSubtree = (node: TreeNode | null): number => {
36 // Base case: empty tree is considered perfect with size 0
37 if (!node) {
38 return 0;
39 }
40
41 // Recursively check left and right subtrees
42 const leftSubtreeSize: number = checkPerfectSubtree(node.left);
43 const rightSubtreeSize: number = checkPerfectSubtree(node.right);
44
45 // If either subtree is not perfect, or they have different sizes,
46 // then current subtree is not perfect
47 if (leftSubtreeSize < 0 || leftSubtreeSize !== rightSubtreeSize) {
48 return -1;
49 }
50
51 // Calculate total nodes in current perfect subtree
52 // (left subtree + right subtree + current node)
53 const currentSubtreeSize: number = leftSubtreeSize + rightSubtreeSize + 1;
54
55 // Store the size of this perfect subtree
56 perfectSubtreeSizes.push(currentSubtreeSize);
57
58 return currentSubtreeSize;
59 };
60
61 // Traverse the tree and collect all perfect subtree sizes
62 checkPerfectSubtree(root);
63
64 // Check if we have at least k perfect subtrees
65 if (perfectSubtreeSizes.length < k) {
66 return -1;
67 }
68
69 // Sort sizes in descending order and return the k-th largest (k-1 index)
70 perfectSubtreeSizes.sort((a: number, b: number) => b - a);
71 return perfectSubtreeSizes[k - 1];
72}
73
Time and Space Complexity
Time Complexity: O(n × log n)
The algorithm consists of two main phases:
-
DFS Traversal: The
dfs
function visits each node in the binary tree exactly once to determine if subtrees are perfect and calculate their sizes. This takesO(n)
time, wheren
is the total number of nodes in the tree. -
Sorting: After collecting all perfect subtree sizes in the
nums
list, the algorithm sorts this list in descending order. In the worst case, if all subtrees are perfect, thenums
list containsn
elements (one for each node representing the perfect subtree rooted at that node). Sortingn
elements takesO(n × log n)
time.
Since O(n × log n)
dominates O(n)
, the overall time complexity is O(n × log n)
.
Space Complexity: O(n)
The space usage comes from:
-
Recursion Stack: The DFS traversal uses the call stack, which in the worst case (skewed tree) can go as deep as
n
levels, requiringO(n)
space. -
nums List: This list stores the sizes of all perfect subtrees found. In the worst case, when the tree is a perfect binary tree, every subtree rooted at each node is also perfect, resulting in
n
entries in the list, requiringO(n)
space. -
Sorting: The sorting operation (likely using Timsort in Python) requires at most
O(n)
auxiliary space.
Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding Perfect Binary Tree Definition
Pitfall: A common mistake is confusing a perfect binary tree with a complete or full binary tree. Developers might incorrectly validate a subtree as perfect by only checking if internal nodes have two children, without verifying that all leaves are at the same depth.
Example of Incorrect Logic:
# WRONG: Only checks if node has 0 or 2 children
def dfs(node):
if not node:
return 0
if not node.left and not node.right: # leaf node
return 1
if node.left and node.right: # has both children
left_size = dfs(node.left)
right_size = dfs(node.right)
return left_size + right_size + 1
return -1 # has only one child
Solution: The correct approach ensures that left and right subtrees have equal sizes (which guarantees equal heights in a perfect tree):
if left_size < 0 or left_size != right_size: return -1
2. Incorrectly Handling Base Case for Empty Trees
Pitfall: Returning -1
for None
nodes instead of 0
, which breaks the logic for identifying leaf nodes as perfect subtrees of size 1.
Incorrect Implementation:
# WRONG: This prevents leaf nodes from being recognized as perfect subtrees
def dfs(node):
if node is None:
return -1 # Should be 0!
# ... rest of logic
Solution: Return 0
for None
nodes. This allows leaf nodes (with two None
children) to be correctly identified as perfect subtrees since both children return 0
(equal sizes).
3. Not Collecting All Perfect Subtrees
Pitfall: Only collecting perfect subtrees when the entire check passes, missing intermediate perfect subtrees when a larger subtree isn't perfect.
Incorrect Approach:
def dfs(node):
if not node:
return 0
left_size = dfs(node.left)
right_size = dfs(node.right)
if left_size < 0 or left_size != right_size:
# WRONG: Not checking if children might be perfect subtrees themselves
return -1
# Only adds current subtree if it's perfect
current_size = left_size + right_size + 1
sizes.append(current_size)
return current_size
Solution: The recursive nature of the correct implementation ensures all perfect subtrees are found because dfs
is called on every node, and each node that forms a perfect subtree adds its size to the list before returning.
4. Off-by-One Error in k-th Element Access
Pitfall: Forgetting that list indices are 0-based when accessing the k-th largest element after sorting.
Incorrect Access:
# WRONG: Returns (k+1)-th largest element return perfect_subtree_sizes[k] # Should be [k-1]
Solution: Use perfect_subtree_sizes[k - 1]
to correctly access the k-th largest element in a 0-indexed list.
5. Not Handling Edge Cases
Pitfall: Failing to check if there are fewer than k
perfect subtrees before attempting to access the k-th element.
Problematic Code:
# WRONG: Will throw IndexError if len(sizes) < k
def kthLargestPerfectSubtree(root, k):
sizes = []
dfs(root)
sizes.sort(reverse=True)
return sizes[k - 1] # IndexError if k > len(sizes)
Solution: Always validate that you have at least k
perfect subtrees:
if len(perfect_subtree_sizes) < k:
return -1
A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".
What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?
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!