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:
- For each node, subtract its value from the current
limit
to track the remaining sum needed - If we reach a leaf node, check if the remaining limit is greater than 0:
- If
limit > 0
, the path sum is insufficient, so returnNone
(delete the node) - Otherwise, keep the node by returning it
- If
- Recursively process left and right subtrees with the updated limit
- After processing children, if both children become
None
(deleted), the current node becomes a leaf and should also be deleted - 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:
- We need to explore complete paths from root to leaf to calculate path sums
- We need to process nodes in a bottom-up manner (leaves first, then parents)
- The recursive nature of DFS allows us to naturally track the cumulative sum as we traverse down the tree
- 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.
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:
- First, recursively process the left and right subtrees with the updated limit (
limit - root.val
) - Each recursive call returns either the subtree root (if at least one sufficient path exists) or
None
(if all paths are insufficient) - After getting results from both children, if both return
None
, the current node has no sufficient paths and should also be deleted - 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:
-
Base Case - Null Node: If the current node is
None
, returnNone
immediately as there's nothing to process. -
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. -
Leaf Node Check: If we reach a leaf node (both
root.left
androot.right
areNone
):- If
limit > 0
, it means the path sum from root to this leaf is insufficient, so returnNone
to delete this leaf - Otherwise, return
root
to keep this leaf in the tree
- If
-
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). -
Parent Node Decision: After processing both children:
- If both
root.left
androot.right
becomeNone
(meaning all paths through both subtrees were insufficient), the current node becomes a leaf with no sufficient paths, so returnNone
- Otherwise, at least one child has a sufficient path, so return
root
to keep this node
- If both
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 EvaluatorExample 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 ✓
- Update limit:
- Process right child (2):
- Update limit:
2 - 2 = 0
- It's a leaf and
0 ≤ 0
, so return node 2 ✓
- Update limit:
- Both children exist, return node 11 ✓
- Update limit:
- 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 ✓
- Update limit:
- 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 returnNone
✗
- Update limit:
- Both children are
None
, node 4 becomes a leaf - Return
None
(delete node 4) ✗
- Update limit:
- 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 beO(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
Which of the following uses divide and conquer strategy?
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!