Insufficient Nodes in Root to Leaf Paths
MedPrompt
Given the root of a binary tree and an integer limit, delete all insufficient nodes in the tree simultaneously, and return the root of the resulting binary tree.
A node is insufficient if every root to leaf path intersecting this node has a sum strictly less than limit.
A leaf is a node with no children.
Example 1:
Input: root = [1,2,3,4,-99,-99,7,8,9,-99,-99,12,13,-99,14], limit = 1
Output: [1,2,3,4,null,null,7,8,9,null,14]Example 2:
Input: root = [5,4,8,11,null,17,4,7,1,null,null,5,3], limit = 22
Output: [5,4,8,11,null,17,4,7,null,null,null,5]Example 3:
Input: root = [1,2,-3,-5,null,4,null], limit = -1
Output: [1,null,-3,4]
Constraints:
- The number of nodes in the tree is in the range
[1, 5000]. -105 <= Node.val <= 105-109 <= limit <= 109
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses two separate traversals of the binary tree. The first pass is to identify and mark every node that lies on at least one 'sufficient' root-to-leaf path (a path with sum >= limit). The second pass then uses this information to reconstruct the tree, pruning any node that was not marked as sufficient.
Algorithm
- Create a
Map<TreeNode, Boolean>to store whether a node is part of a sufficient path. - Pass 1: Mark Sufficient Nodes
- Implement a recursive DFS function
isNodeSufficient(node, currentSum). - This function traverses the tree in a post-order fashion.
- For a leaf node, it checks if the path sum
currentSum + node.valis>= limit. It stores this boolean result in the map and returns it. - For an internal node, it recursively calls itself for its left and right children.
- A node is considered sufficient if either its left or right subtree contains a sufficient path (
leftSufficient || rightSufficient). This result is stored in the map and returned. - Initiate this pass by calling
isNodeSufficient(root, 0).
- Implement a recursive DFS function
- Pass 2: Rebuild the Tree
- Implement a second recursive function
rebuild(node). - This function traverses the tree and constructs the final version.
- For each
node, it checks the map. If the map indicates the node is insufficient (sufficientMap.get(node)is false), it returnsnull, effectively pruning this node and its entire subtree. - If the node is sufficient, it recursively calls
rebuildfor its children:node.left = rebuild(node.left)andnode.right = rebuild(node.right). - The final result is the tree returned by
rebuild(root).
- Implement a second recursive function
Walkthrough
In this method, we first need to determine for every single node whether it's part of a sufficient path. A node is sufficient if at least one root-to-leaf path passing through it has a sum greater than or equal to limit. We can use a post-order traversal to compute this for all nodes and store the results in a hash map.
Once we have this information, we perform a second traversal (e.g., pre-order) to build the new tree. During this traversal, if we encounter a node that our map indicates is insufficient, we prune it by returning null to its parent. Otherwise, we keep the node and continue the process for its children.
import java.util.HashMap;import java.util.Map; class Solution { private Map<TreeNode, Boolean> sufficientMap = new HashMap<>(); private int limit; public TreeNode sufficientSubset(TreeNode root, int limit) { this.limit = limit; if (root == null) { return null; } // Pass 1: Populate the map with sufficiency information for each node. isNodeSufficient(root, 0); // Pass 2: Rebuild the tree by pruning insufficient nodes. return rebuild(root); } // Pass 1: Returns true if the subtree at 'node' contains a sufficient path. private boolean isNodeSufficient(TreeNode node, long sum) { if (node == null) { return false; } sum += node.val; if (node.left == null && node.right == null) { // Leaf node boolean sufficient = sum >= limit; sufficientMap.put(node, sufficient); return sufficient; } boolean leftSufficient = isNodeSufficient(node.left, sum); boolean rightSufficient = isNodeSufficient(node.right, sum); // A node is on a sufficient path if any of its descendant paths are sufficient. boolean isSufficient = leftSufficient || rightSufficient; sufficientMap.put(node, isSufficient); return isSufficient; } // Pass 2: Returns the modified tree node, or null if it should be pruned. private TreeNode rebuild(TreeNode node) { if (node == null) { return null; } // If a node is not on any sufficient path, prune it. if (!sufficientMap.getOrDefault(node, false)) { return null; } node.left = rebuild(node.left); node.right = rebuild(node.right); return node; }}Complexity
Time
O(N), where N is the number of nodes. The first pass visits each node once, and the second pass also visits each node once, leading to a total time complexity of O(N) + O(N) = O(N).
Space
O(N), where N is the number of nodes in the tree. This is because we use a hash map to store the sufficiency status for every node.
Trade-offs
Pros
The logic is separated into two distinct, clear steps: identification and modification, which can be easier to reason about.
Cons
Requires O(N) extra space for the hash map, making it less space-efficient than the single-pass approach.
Involves two separate traversals of the tree, which is less elegant and slightly more complex to implement.
This optimal approach solves the problem in a single post-order traversal of the tree. A recursive function is used to simultaneously check for path sufficiency and prune the tree in-place. The decision to keep or remove a node is made after visiting its children, which is the essence of a post-order traversal.
Algorithm
- Define a single recursive helper function, e.g.,
isSufficient(node, sum, limit), that returns a boolean. - This function will perform a post-order traversal. For a given
node, it first makes recursive calls for its left and right children. - The
sumparameter tracks the sum of node values from the root to the parent of the currentnode. - Base Case: If
nodeis a leaf (node.left == null && node.right == null), the function checks if the total path sum (sum + node.val) is>= limit. It returnstrueif sufficient,falseotherwise. - Recursive Step: For an internal node:
- Recursively call
isSufficientfor the left child:leftSufficient = isSufficient(node.left, sum + node.val, limit). Handle the case wherenode.leftis null (which should result infalse). - Recursively call
isSufficientfor the right child:rightSufficient = isSufficient(node.right, sum + node.val, limit). - After the recursive calls return, use their boolean results to prune the tree. If
leftSufficientisfalse, setnode.left = null. IfrightSufficientisfalse, setnode.right = null. - The function then returns
leftSufficient || rightSufficient. This boolean indicates to the parent caller whether the currentnoderemains part of a sufficient path after potential pruning of its children.
- Recursively call
- In the main function, call
isSufficient(root, 0, limit). If this initial call returnsfalse, it means the root itself is insufficient, so the entire tree is deleted. In this case, returnnull. Otherwise, return the modifiedroot.
Walkthrough
We can determine if a node is insufficient and prune it in a single pass. The key idea is that a node's sufficiency depends on its children. This suggests a post-order traversal approach.
We define a recursive helper function that takes a node and the sum of the path from the root to its parent. This function will do two things: prune the children if they are roots of insufficient subtrees, and return a boolean indicating if the current node is part of any sufficient path.
For any node, we first recurse on its children. The boolean returned from the recursive calls tells us if a sufficient path exists through the left or right child. If a child's recursive call returns false, it means all paths through that child are insufficient, so we can prune that child by setting the parent's link to null. After handling the children, the current node is sufficient if at least one of its children was on a sufficient path. The base case is a leaf node, where we simply check if its root-to-leaf path sum meets the limit.
class Solution { public TreeNode sufficientSubset(TreeNode root, int limit) { // The helper function returns false if the entire tree starting from root is insufficient. if (!isSufficient(root, 0, limit)) { return null; } return root; } /** * Performs post-order traversal to prune insufficient nodes. * @param node The current node. * @param sum The sum of values from the root to the parent of `node`. * @param limit The threshold. * @return true if `node` is part of a sufficient path, false otherwise. */ private boolean isSufficient(TreeNode node, long sum, int limit) { if (node == null) { return false; } // If it's a leaf node, check if the path sum is sufficient. This is the base case. if (node.left == null && node.right == null) { return (sum + node.val) >= limit; } // Recursively check left and right subtrees. The sum for the children's path // includes the current node's value. boolean leftIsSufficient = isSufficient(node.left, sum + node.val, limit); boolean rightIsSufficient = isSufficient(node.right, sum + node.val, limit); // If the left subtree does not have any sufficient path, prune it. if (!leftIsSufficient) { node.left = null; } // If the right subtree does not have any sufficient path, prune it. if (!rightIsSufficient) { node.right = null; } // The current node is part of a sufficient path if either of its subtrees // (after pruning) contains a sufficient path. return leftIsSufficient || rightIsSufficient; }}Complexity
Time
O(N), where N is the number of nodes in the tree, as we visit each node exactly once.
Space
O(H), where H is the height of the tree. This space is used by the recursion stack. In the worst case of a skewed tree, this can be O(N).
Trade-offs
Pros
Highly efficient, solving the problem in a single pass.
Space complexity is optimal, only using O(H) space for the recursion stack.
Modifies the tree in-place, avoiding the need for extra data structures to store intermediate results.
Cons
The recursive function has a side effect (modifying the tree), which can sometimes make the code slightly harder to reason about compared to a pure function.
The combined logic of checking and pruning in a single function can be less intuitive for beginners.
Solutions
Solution
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { public TreeNode sufficientSubset ( TreeNode root , int limit ) { if ( root == null ) { return null ; } limit -= root . val ; if ( root . left == null && root . right == null ) { return limit > 0 ? null : root ; } root . left = sufficientSubset ( root . left , limit ); root . right = sufficientSubset ( root . right , limit ); return root . left == null && root . right == null ? null : root ; } }/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */ /** * @param {TreeNode} root * @param {number} limit * @return {TreeNode} */ var sufficientSubset = function (root, limit) { if (root === null) { return null; } limit -= root.val; if (root.left === null && root.right === null) { return limit > 0 ? null : root; } root.left = sufficientSubset(root.left, limit); root.right = sufficientSubset(root.right, limit); return root.left === null && root.right === null ? null : root; };/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */ class Solution { public: TreeNode * sufficientSubset ( TreeNode * root , int limit ) { if ( ! root ) { return nullptr ; } limit -= root -> val ; if ( ! root -> left && ! root -> right ) { return limit > 0 ? nullptr : root ; } root -> left = sufficientSubset ( root -> left , limit ); root -> right = sufficientSubset ( root -> right , limit ); return ! root -> left && ! root -> right ? nullptr : root ; } };# Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution : def sufficientSubset ( self , root : Optional [ TreeNode ], limit : int ) -> Optional [ TreeNode ]: 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 root . left = self . sufficientSubset ( root . left , limit ) root . right = self . sufficientSubset ( root . right , limit ) return None if root . left is None and root . right is None else rootVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.