Facebook Pixel

1080. Insufficient Nodes in Root to Leaf Paths

Problem Description

You are given a binary tree and an integer value called limit. Your task is to remove all "insufficient" nodes from the tree and return the modified tree's root.

A node is considered insufficient if every path from the root to any leaf that passes through this node has a total sum that is strictly less than the limit. In other words, if all root-to-leaf paths going through a particular node have sums below the threshold, that node should be deleted.

Key points to understand:

  • A leaf is a node that has no children (both left and right children are null)
  • The deletion happens simultaneously for all insufficient nodes
  • A path sum is calculated by adding all node values from root to a leaf
  • If a node becomes a leaf after its children are deleted (due to being insufficient), it must also be checked against the limit

The solution uses a recursive approach with these steps:

  1. For each node, subtract its value from the current limit to track the remaining sum needed
  2. If we reach a leaf node, check if the remaining limit is greater than 0:
    • If limit > 0, the path sum is insufficient, so return None (delete the node)
    • Otherwise, keep the node by returning it
  3. Recursively process left and right subtrees with the updated limit
  4. After processing children, if both children become None (deleted), the current node becomes a leaf and should also be deleted
  5. Return None if both children are deleted, otherwise return the current node

The algorithm effectively prunes the tree from bottom to top, removing any branches where all paths fail to meet the required sum threshold.

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 with nodes (tree nodes) and edges (parent-child relationships).

Is it a tree?

  • Yes: The problem explicitly states we're working with a binary tree structure where we need to traverse from root to leaf nodes.

DFS

  • Conclusion: Since we're dealing with a tree structure, the flowchart leads us directly to DFS (Depth-First Search).

The DFS pattern is the perfect fit for this problem because:

  1. We need to explore complete paths from root to leaf to calculate path sums
  2. We need to process nodes in a bottom-up manner (leaves first, then parents)
  3. The recursive nature of DFS allows us to naturally track the cumulative sum as we traverse down the tree
  4. DFS enables us to make decisions about parent nodes based on the results from their children

Conclusion: The flowchart confirms that DFS is the appropriate algorithm for solving the "Insufficient Nodes in Root to Leaf Paths" problem, as it allows us to traverse each root-to-leaf path completely, evaluate the path sum against the limit, and recursively prune insufficient nodes from bottom to top.

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

Intuition

The key insight is to think about this problem from the perspective of each node asking: "Should I exist in the final tree?"

A node should only exist if at least one path through it reaches a leaf with a sufficient sum. This immediately suggests a bottom-up approach - we can't decide about a parent until we know about its children.

Consider how path sums work: as we traverse down from root to leaf, we accumulate the sum. Instead of tracking the running sum, we can flip our perspective and track "how much more do we need?" by subtracting each node's value from the limit as we go down. This transforms our question at each leaf to simply: "Is the remaining limit > 0?" If yes, we haven't accumulated enough sum, so this path is insufficient.

The clever part comes when we think about deletion recursively. When we delete all insufficient leaves from a subtree, some parents might become new leaves. These new leaves must also be checked - if both children of a node get deleted, that node becomes a leaf and is insufficient (since all paths through it were insufficient).

This naturally leads to a post-order traversal pattern:

  1. First, recursively process the left and right subtrees with the updated limit (limit - root.val)
  2. Each recursive call returns either the subtree root (if at least one sufficient path exists) or None (if all paths are insufficient)
  3. After getting results from both children, if both return None, the current node has no sufficient paths and should also be deleted
  4. The base case is when we reach an actual leaf - we simply check if we've accumulated enough sum

This approach elegantly handles the "simultaneous deletion" requirement because we process the tree bottom-up, making deletion decisions based on the already-processed children.

Learn more about Tree, Depth-First Search and Binary Tree patterns.

Solution Approach

The solution implements a recursive DFS approach with post-order traversal to efficiently prune insufficient nodes from the binary tree.

Core Algorithm Steps:

  1. Base Case - Null Node: If the current node is None, return None immediately as there's nothing to process.

  2. Update Remaining Limit: Subtract the current node's value from the limit: limit -= root.val. This tracks how much more sum we need to reach the threshold on paths below this node.

  3. Leaf Node Check: If we reach a leaf node (both root.left and root.right are None):

    • If limit > 0, it means the path sum from root to this leaf is insufficient, so return None to delete this leaf
    • Otherwise, return root to keep this leaf in the tree
  4. Recursive Processing: For non-leaf nodes, recursively process both subtrees:

    root.left = self.sufficientSubset(root.left, limit)
    root.right = self.sufficientSubset(root.right, limit)

    Each recursive call passes the updated limit and returns either the subtree (if it contains at least one sufficient path) or None (if all paths are insufficient).

  5. Parent Node Decision: After processing both children:

    • If both root.left and root.right become None (meaning all paths through both subtrees were insufficient), the current node becomes a leaf with no sufficient paths, so return None
    • Otherwise, at least one child has a sufficient path, so return root to keep this node

Why This Works:

  • The post-order traversal ensures we process children before parents, allowing bottom-up pruning
  • By passing limit - root.val to children, we elegantly track the remaining sum needed without maintaining a separate path sum variable
  • The algorithm automatically handles cascading deletions: when a node's children are deleted, it becomes a leaf and gets evaluated for deletion
  • Time complexity is O(n) where n is the number of nodes, as we visit each node once
  • Space complexity is O(h) where h is the height of the tree, due to the recursion stack

The beauty of this solution lies in its simplicity - by framing the problem as "how much more sum do we need?" rather than "what's our current sum?", the code becomes remarkably clean and intuitive.

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 see how the algorithm works.

Consider this binary tree with limit = 22:

        5
       / \
      4   8
     /   / \
    11  13  4
   / \      \
  7   2      1

We need to remove all nodes where every root-to-leaf path through them has sum < 22.

Step-by-step execution:

Starting at root (5), we call sufficientSubset(root, 22):

  • Update limit: 22 - 5 = 17
  • Not a leaf, so recursively process children

Left subtree of 5 (node 4):

  • Update limit: 17 - 4 = 13
  • Process its child (11):
    • Update limit: 13 - 11 = 2
    • Process left child (7):
      • Update limit: 2 - 7 = -5
      • It's a leaf and -5 ≤ 0, so return node 7 ✓
    • Process right child (2):
      • Update limit: 2 - 2 = 0
      • It's a leaf and 0 ≤ 0, so return node 2 ✓
    • Both children exist, return node 11 ✓
  • Child exists, return node 4 ✓

Right subtree of 5 (node 8):

  • Update limit: 17 - 8 = 9
  • Process left child (13):
    • Update limit: 9 - 13 = -4
    • It's a leaf and -4 ≤ 0, so return node 13 ✓
  • Process right child (4):
    • Update limit: 9 - 4 = 5
    • Process its child (1):
      • Update limit: 5 - 1 = 4
      • It's a leaf and 4 > 0, so return None
    • Both children are None, node 4 becomes a leaf
    • Return None (delete node 4) ✗
  • Left child exists but right is None, return node 8 ✓

Back at root (5):

  • Both children exist, return root 5 ✓

Final tree:

        5
       / \
      4   8
     /   /
    11  13
   / \
  7   2

The paths and their sums:

  • 5→4→11→7: sum = 27 ≥ 22 ✓
  • 5→4→11→2: sum = 22 ≥ 22 ✓
  • 5→8→13: sum = 26 ≥ 22 ✓
  • 5→8→4→1: sum = 18 < 22 ✗ (removed)

Notice how node 4 (right child of 8) was removed after its child was deleted, demonstrating the cascading deletion behavior. The algorithm correctly identified that the only path through that branch (5→8→4→1) had insufficient sum.

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
10
11class Solution:
12    def sufficientSubset(
13        self, root: Optional[TreeNode], limit: int
14    ) -> Optional[TreeNode]:
15        """
16        Remove all insufficient nodes from the binary tree.
17        A node is insufficient if every root-to-leaf path going through this node
18        has a sum strictly less than the limit.
19      
20        Args:
21            root: Root node of the binary tree
22            limit: The threshold value for path sums
23          
24        Returns:
25            Root of the modified tree with insufficient nodes removed
26        """
27        # Base case: empty tree
28        if root is None:
29            return None
30      
31        # Subtract current node's value from limit for recursive calls
32        # This tracks the remaining sum needed for paths below this node
33        limit -= root.val
34      
35        # Leaf node case: check if the path sum meets the threshold
36        if root.left is None and root.right is None:
37            # If limit > 0, the path sum is insufficient (sum < original limit)
38            # Return None to remove this leaf, otherwise keep it
39            return None if limit > 0 else root
40      
41        # Recursively process left and right subtrees
42        # Pass the updated limit to check sufficiency of child paths
43        root.left = self.sufficientSubset(root.left, limit)
44        root.right = self.sufficientSubset(root.right, limit)
45      
46        # After processing children, check if current node should be removed
47        # If both children were removed (both are None), this node leads to no sufficient paths
48        # Remove it by returning None, otherwise keep the node
49        return None if root.left is None and root.right is None else root
50
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    /**
18     * Removes all insufficient nodes from the binary tree.
19     * A node is insufficient if every root-to-leaf path through that node
20     * has a sum strictly less than the limit.
21     * 
22     * @param root The root of the binary tree
23     * @param limit The minimum sum threshold for root-to-leaf paths
24     * @return The root of the pruned tree, or null if the entire tree is insufficient
25     */
26    public TreeNode sufficientSubset(TreeNode root, int limit) {
27        // Base case: empty tree
28        if (root == null) {
29            return null;
30        }
31      
32        // Subtract current node's value from limit for recursive calls
33        // This tracks the remaining sum needed for the path to be sufficient
34        int remainingLimit = limit - root.val;
35      
36        // Leaf node case: check if the path sum meets the threshold
37        if (root.left == null && root.right == null) {
38            // If remainingLimit > 0, the path sum is insufficient
39            // Return null to remove this leaf, otherwise keep it
40            return remainingLimit > 0 ? null : root;
41        }
42      
43        // Recursively process left and right subtrees
44        // Each recursive call will prune insufficient nodes in that subtree
45        root.left = sufficientSubset(root.left, remainingLimit);
46        root.right = sufficientSubset(root.right, remainingLimit);
47      
48        // After processing children, check if current node became a leaf
49        // If both children were removed (both null), this node is now insufficient
50        // Remove it by returning null, otherwise keep the node
51        return (root.left == null && root.right == null) ? null : root;
52    }
53}
54
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    /**
15     * Removes all insufficient nodes from the binary tree.
16     * A node is insufficient if every root-to-leaf path through that node has a sum strictly less than limit.
17     * 
18     * @param root The root of the binary tree
19     * @param limit The threshold value for path sums
20     * @return The root of the modified tree, or nullptr if the entire tree is insufficient
21     */
22    TreeNode* sufficientSubset(TreeNode* root, int limit) {
23        // Base case: empty tree
24        if (root == nullptr) {
25            return nullptr;
26        }
27      
28        // Update limit by subtracting current node's value
29        // This represents the remaining sum needed from this node to leaf
30        limit -= root->val;
31      
32        // Leaf node case: check if the path sum is sufficient
33        if (root->left == nullptr && root->right == nullptr) {
34            // If limit > 0, the path sum is insufficient (less than original limit)
35            // Return nullptr to delete this leaf, otherwise keep it
36            return (limit > 0) ? nullptr : root;
37        }
38      
39        // Recursively process left and right subtrees
40        // Each subtree will be pruned of insufficient nodes
41        root->left = sufficientSubset(root->left, limit);
42        root->right = sufficientSubset(root->right, limit);
43      
44        // After processing children, check if current node should be deleted
45        // If both children are deleted (nullptr), current node becomes a dead end
46        // and should also be deleted
47        return (root->left == nullptr && root->right == nullptr) ? nullptr : root;
48    }
49};
50
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 * Removes all insufficient nodes from the binary tree.
17 * A node is insufficient if every root-to-leaf path through that node has a sum strictly less than the limit.
18 * 
19 * @param root - The root node of the binary tree
20 * @param limit - The threshold value for path sums
21 * @returns The root of the modified tree, or null if the entire tree is insufficient
22 */
23function sufficientSubset(root: TreeNode | null, limit: number): TreeNode | null {
24    // Base case: empty tree
25    if (root === null) {
26        return null;
27    }
28  
29    // Subtract current node's value from limit for recursive calls
30    const remainingLimit: number = limit - root.val;
31  
32    // Leaf node: check if the path sum meets the threshold
33    if (root.left === null && root.right === null) {
34        // If remaining limit > 0, path sum is insufficient
35        return remainingLimit > 0 ? null : root;
36    }
37  
38    // Recursively process left and right subtrees with updated limit
39    root.left = sufficientSubset(root.left, remainingLimit);
40    root.right = sufficientSubset(root.right, remainingLimit);
41  
42    // If both children were removed (insufficient), remove current node as well
43    // Otherwise, keep the current node since at least one sufficient path exists
44    return root.left === null && root.right === null ? null : root;
45}
46

Time and Space Complexity

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

The algorithm performs a post-order traversal of the binary tree, visiting each node exactly once. At each node, the operations performed are:

  • Subtracting the node's value from the limit: O(1)
  • Checking if the node is a leaf: O(1)
  • Making recursive calls to left and right children
  • Checking if both children are None after recursion: O(1)

Since each node is visited once and only constant-time operations are performed at each node (excluding the recursive calls), the total time complexity is O(n).

Space Complexity: O(h) where h is the height of the binary tree.

The space complexity is determined by:

  • Recursion call stack: The maximum depth of the recursion equals the height of the tree. In the worst case (skewed tree), this could be O(n), while in the best case (balanced tree), it would be O(log n).
  • Auxiliary space: The algorithm uses only a constant amount of extra space for variables like the modified limit value at each recursive call.

Therefore, the space complexity is O(h), which ranges from O(log n) for a balanced tree to O(n) for a completely skewed tree.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Incorrectly Handling the Path Sum Calculation

The Mistake: Many developers mistakenly try to track the cumulative sum from root to current node by adding values as they traverse down, rather than subtracting from the limit:

# INCORRECT approach
def sufficientSubset(self, root, limit, current_sum=0):
    if root is None:
        return None
  
    current_sum += root.val  # Adding to track sum
  
    if root.left is None and root.right is None:
        # Wrong comparison logic
        return root if current_sum >= limit else None
  
    # Passing accumulated sum instead of remaining limit
    root.left = self.sufficientSubset(root.left, limit, current_sum)
    root.right = self.sufficientSubset(root.right, limit, current_sum)
    # ...

Why It's Wrong:

  • Makes the code more complex with an extra parameter
  • Easy to make errors in the comparison logic (using >= vs > incorrectly)
  • Harder to reason about the remaining requirement

The Fix: Subtract from limit instead of accumulating sum. This elegantly tracks "how much more do we need?" rather than "how much do we have?":

# CORRECT approach
limit -= root.val  # Subtract to track remaining needed
if root.left is None and root.right is None:
    return None if limit > 0 else root  # Clear logic

Pitfall 2: Forgetting to Update Parent Node References After Deletion

The Mistake: Simply returning None for insufficient nodes without updating the parent's child references:

# INCORRECT - doesn't update parent references
def sufficientSubset(self, root, limit):
    if root is None:
        return None
  
    limit -= root.val
  
    if root.left is None and root.right is None:
        return None if limit > 0 else root
  
    # Recursively process but don't update references!
    self.sufficientSubset(root.left, limit)   # WRONG
    self.sufficientSubset(root.right, limit)  # WRONG
  
    return None if root.left is None and root.right is None else root

Why It's Wrong:

  • The tree structure isn't actually modified
  • Parent nodes still point to "deleted" children
  • The final tree will contain nodes that should have been removed

The Fix: Always reassign the child references with the return value of recursive calls:

# CORRECT - updates the tree structure
root.left = self.sufficientSubset(root.left, limit)
root.right = self.sufficientSubset(root.right, limit)

Pitfall 3: Incorrect Order of Operations

The Mistake: Checking if a node should be deleted before processing its children:

# INCORRECT - premature deletion check
def sufficientSubset(self, root, limit):
    if root is None:
        return None
  
    limit -= root.val
  
    # Wrong: checking deletion before processing children
    if limit > 0:  # This is NOT how to check if a node is insufficient!
        return None
  
    root.left = self.sufficientSubset(root.left, limit)
    root.right = self.sufficientSubset(root.right, limit)
    # ...

Why It's Wrong:

  • A non-leaf node's sufficiency depends on whether ANY path through it is sufficient
  • We can't know this until we've checked all paths (processed all children)
  • The limit > 0 check only makes sense for leaf nodes

The Fix: Process children first (post-order), then decide about the current node based on whether any children remain:

# CORRECT order
# 1. Process leaf nodes immediately
if root.left is None and root.right is None:
    return None if limit > 0 else root

# 2. Process children for non-leaf nodes
root.left = self.sufficientSubset(root.left, limit)
root.right = self.sufficientSubset(root.right, limit)

# 3. Then decide about current node based on children
return None if root.left is None and root.right is None else root
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following uses divide and conquer strategy?


Recommended Readings

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

Load More