Facebook Pixel

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:

  1. Identify all perfect binary subtrees within the given binary tree
  2. Determine the size of each perfect binary subtree (size = total number of nodes in that subtree)
  3. Find the k-th largest among these sizes
  4. Return this k-th largest size, or -1 if fewer than k 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:

  1. We need to traverse the entire tree to identify all perfect binary subtrees
  2. For each node, we need to check if the subtree rooted at that node forms a perfect binary tree
  3. DFS allows us to process each subtree from the bottom up, calculating the size of perfect subtrees recursively
  4. 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.

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

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:

  1. Recursively calculate the perfect subtree size for left and right children
  2. 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
  3. 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:

  1. Base Case: If the current node is None, return 0 (an empty tree has size 0)

  2. Recursive Calls: Calculate the sizes of left and right subtrees:

    l, r = dfs(root.left), dfs(root.right)
  3. 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
  4. Size Calculation: If the subtree is perfect, calculate its total size:

    cnt = l + r + 1  # left subtree + right subtree + current node
  5. Store Perfect Subtree Size: Add the size to our collection:

    nums.append(cnt)
  6. Return Size: Return cnt so parent nodes can use this information

Main Algorithm Flow

  1. Initialize Storage: Create an empty list nums to store all perfect subtree sizes

  2. Traverse Tree: Call dfs(root) to process the entire tree and populate nums

  3. Check Availability: If we have fewer than k perfect subtrees, return -1:

    if len(nums) < k:
        return -1
  4. 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 Evaluator

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

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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

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:

  1. 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 takes O(n) time, where n is the total number of nodes in the tree.

  2. 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, the nums list contains n elements (one for each node representing the perfect subtree rooted at that node). Sorting n elements takes O(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:

  1. Recursion Stack: The DFS traversal uses the call stack, which in the worst case (skewed tree) can go as deep as n levels, requiring O(n) space.

  2. 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, requiring O(n) space.

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

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

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

Load More