Path Sum III
MedPrompt
Given the root of a binary tree and an integer targetSum, return the number of paths where the sum of the values along the path equals targetSum.
The path does not need to start or end at the root or a leaf, but it must go downwards (i.e., traveling only from parent nodes to child nodes).
Example 1:
Input: root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
Output: 3
Explanation: The paths that sum to 8 are shown.Example 2:
Input: root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
Output: 3
Constraints:
- The number of nodes in the tree is in the range
[0, 1000]. -109 <= Node.val <= 109-1000 <= targetSum <= 1000
Approaches
2 approaches with complexity analysis and trade-offs.
This approach is a straightforward, brute-force solution that directly translates the problem statement into a recursive algorithm. It considers every possible starting node for a path. For each node in the tree, it initiates a separate downward traversal to find all paths starting from that node that sum up to the targetSum. The final result is the sum of counts from all possible starting nodes.
Algorithm
The overall strategy involves a double recursion.
- The main recursive function,
pathSum(node, targetSum), traverses every node in the tree. - For each
node, it calculates the total number of valid paths by summing up three values:- The number of valid paths that start at the current
node. - The number of valid paths found in the left subtree (by recursively calling
pathSumonnode.left). - The number of valid paths found in the right subtree (by recursively calling
pathSumonnode.right).
- The number of valid paths that start at the current
- A helper function,
countPathsFromNode(node, currentSum, targetSum), is used to find the number of paths that start at a specificnodeand sum totargetSum. - This helper function traverses downwards from the given
node.- It takes the current node, the sum accumulated so far on the current path (
currentSum), and thetargetSum. - If
currentSum + node.valequalstargetSum, a valid path is found. - It continues to search for longer paths by recursively calling itself on the left and right children with the updated
currentSum.
- It takes the current node, the sum accumulated so far on the current path (
Walkthrough
The solution uses a pair of recursive functions. The main function, pathSum, iterates through all nodes in the tree. For each node, it treats it as a potential starting point for a valid path. It calls a helper function, countPathsFromNode, to count all valid paths that begin at this specific node and extend downwards.
The pathSum function's result for a given node is the sum of:
- The number of valid paths starting at the current node (calculated by
countPathsFromNode). - The number of valid paths that exist entirely within the left subtree (found by a recursive call to
pathSumon the left child). - The number of valid paths that exist entirely within the right subtree (found by a recursive call to
pathSumon the right child).
This method ensures that every possible downward path is considered. To prevent integer overflow with potentially large node values and path lengths, path sums are accumulated using a long data type.
/** * 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 int pathSum(TreeNode root, int targetSum) { if (root == null) { return 0; } // Count paths starting from the current root, plus paths in left/right subtrees. return countPathsFromNode(root, targetSum) + pathSum(root.left, targetSum) + pathSum(root.right, targetSum); } // Helper function to count paths starting from a given node that sum to the target. // Note: We use a long for the current sum to avoid overflow. private int countPathsFromNode(TreeNode node, long targetSum) { if (node == null) { return 0; } int count = 0; // Check if the current node itself completes a path. if (node.val == targetSum) { count = 1; } // Continue the path downwards and see if longer paths also sum to the target. long remainingSum = targetSum - node.val; count += countPathsFromNode(node.left, remainingSum); count += countPathsFromNode(node.right, remainingSum); return count; }}Complexity
Time
O(N^2) in the worst case (a skewed tree) and O(N log N) in the average case (a balanced tree), where N is the number of nodes. For each of the N nodes, we traverse its subtree, which takes O(H) time, where H is the height of the subtree.
Space
O(N) in the worst case (a skewed tree) and O(log N) in the average case (a balanced tree). The space is determined by the maximum depth of the recursion stack.
Trade-offs
Pros
Relatively simple to conceptualize and implement.
The logic directly follows the problem's definition without complex data structures.
Cons
Highly inefficient due to a large number of redundant calculations. The sum for the same subpath is computed multiple times.
The time complexity is quadratic in the worst-case scenario, which can be too slow for large trees.
This optimized approach leverages the concept of prefix sums to solve the problem in a single pass. As we traverse the tree from the root downwards, we maintain the sum of the path from the root to the current node (the prefix sum). The sum of any path between an ancestor node A and a descendant node B can be calculated as prefixSum(B) - prefixSum(A). We are looking for paths where this difference equals targetSum.
By rearranging the equation to prefixSum(A) = prefixSum(B) - targetSum, we can see that for each node B, we just need to find how many of its ancestors A have a prefix sum that matches this required value. A hash map is used to efficiently store and retrieve the frequencies of prefix sums encountered on the current path from the root.
Algorithm
- Initialize a global
countto 0 and aHashMap<Long, Integer>namedprefixSumCountto store the frequencies of prefix sums encountered. - Add an initial entry
{0L: 1}toprefixSumCount. This is a crucial base case that handles paths starting from the root node. - Define a recursive DFS function,
dfs(node, currentSum). - Inside
dfs: a. Ifnodeis null, return. b. Update thecurrentSumby adding the currentnode.val. ThecurrentSumrepresents the prefix sum from the root to the current node. c. Calculate thecomplementsum needed:complement = currentSum - targetSum. Check if thiscomplementexists as a key inprefixSumCount. If it does, it means there areprefixSumCount.get(complement)paths ending at the current node that sum totargetSum. Add this number to the globalcount. d. Update the map for the current path: Increment the frequency ofcurrentSuminprefixSumCount. e. Recursively calldfsfor the left and right children. f. Backtrack: After the recursive calls for the children return, decrement the frequency ofcurrentSuminprefixSumCount. This step is vital to ensure that prefix sums from one branch do not interfere with calculations in a sibling branch.
Walkthrough
This method performs a single depth-first traversal (DFS) of the tree. A hash map, prefixSumCount, is used to keep track of the cumulative sums from the root to the nodes on the current path and their frequencies.
For any node curr, the path sum from the root to curr is currentSum. If there was a previous node prev on the same path from the root with a path sum of prevSum, then the sum of the path from prev to curr is currentSum - prevSum. We want this to be equal to targetSum, so currentSum - prevSum = targetSum, which implies prevSum = currentSum - targetSum.
So, as we traverse to a node curr, we calculate its currentSum. Then, we check the hash map to see how many times we have previously seen a prefix sum of currentSum - targetSum. This count gives us the number of valid paths ending at the current node curr.
After processing a node, we add its currentSum to the map before visiting its children. After the recursive calls to its children return (i.e., after exploring its entire subtree), we must remove its currentSum from the map (backtrack). This is essential because the prefix sums of one subtree should not be available to its sibling subtree.
/** * 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; * } * } */import java.util.HashMap;import java.util.Map; class Solution { int count = 0; int k; Map<Long, Integer> prefixSumCount = new HashMap<>(); public int pathSum(TreeNode root, int targetSum) { this.k = targetSum; // The map stores <prefix sum, frequency>. // A prefix sum of 0 has a frequency of 1 to handle paths that start from the root. prefixSumCount.put(0L, 1); dfs(root, 0L); return count; } private void dfs(TreeNode node, long currentSum) { if (node == null) { return; } // 1. Update current path sum currentSum += node.val; // 2. Check if (currentSum - k) exists in the map // This is the core logic. If it exists, it means there's a path // from some ancestor to the current node that sums to k. count += prefixSumCount.getOrDefault(currentSum - k, 0); // 3. Update the map with the current prefix sum for descendant nodes prefixSumCount.put(currentSum, prefixSumCount.getOrDefault(currentSum, 0) + 1); // 4. Recurse for children dfs(node.left, currentSum); dfs(node.right, currentSum); // 5. Backtrack: remove the current prefix sum from the map // This is crucial so that prefix sums from one branch don't affect sibling branches. prefixSumCount.put(currentSum, prefixSumCount.get(currentSum) - 1); }}Complexity
Time
O(N), where N is the number of nodes in the tree. Each node is visited exactly once, and the hash map operations (get, put) take O(1) time on average.
Space
O(H), where H is the height of the tree. This space is used by both the recursion stack and the hash map. In the worst case of a skewed tree, H = N, leading to O(N) space. For a balanced tree, H = log N, leading to O(log N) space.
Trade-offs
Pros
Highly efficient, with a linear time complexity as it traverses the tree only once.
Avoids redundant calculations by storing intermediate results (prefix sums) in a hash map.
Cons
The logic, particularly the concept of prefix sums in a tree and the necessity of backtracking, is more complex to understand.
Requires additional space for the hash map, which can be proportional to the height of the tree.
Solutions
Solution
/** * Definition for a binary tree node. * public class TreeNode { * public int val; * public TreeNode left; * public TreeNode right; * public TreeNode(int val=0, TreeNode left=null, TreeNode right=null) { * this.val = val; * this.left = left; * this.right = right; * } * } */ public class Solution { public int PathSum ( TreeNode root , int targetSum ) { Dictionary < long , int > cnt = new Dictionary < long , int >(); int Dfs ( TreeNode node , long s ) { if ( node == null ) { return 0 ; } s += node . val ; int ans = cnt . GetValueOrDefault ( s - targetSum , 0 ); cnt [ s ] = cnt . GetValueOrDefault ( s , 0 ) + 1 ; ans += Dfs ( node . left , s ); ans += Dfs ( node . right , s ); cnt [ s ]--; return ans ; } cnt [ 0 ] = 1 ; return Dfs ( root , 0 ); } }/** * 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 { private Map < Long , Integer > cnt = new HashMap <>(); private int targetSum ; public int pathSum ( TreeNode root , int targetSum ) { cnt . put ( 0L , 1 ); this . targetSum = targetSum ; return dfs ( root , 0 ); } private int dfs ( TreeNode node , long s ) { if ( node == null ) { return 0 ; } s += node . val ; int ans = cnt . getOrDefault ( s - targetSum , 0 ); cnt . merge ( s , 1 , Integer: : sum ); ans += dfs ( node . left , s ); ans += dfs ( node . right , s ); cnt . merge ( s , - 1 , Integer: : sum ); return ans ; } }/** * 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} targetSum * @return {number} */ var pathSum = function (root, targetSum) { const cnt = new Map(); const dfs = (node, s) => { if (!node) { return 0; } s += node.val; let ans = cnt.get(s - targetSum) || 0; cnt.set(s, (cnt.get(s) || 0) + 1); ans += dfs(node.left, s); ans += dfs(node.right, s); cnt.set(s, cnt.get(s) - 1); return ans; }; cnt.set(0, 1); return dfs(root, 0); };/** * 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: int pathSum ( TreeNode * root , int targetSum ) { unordered_map < long , int > cnt ; cnt [ 0 ] = 1 ; function < int ( TreeNode * , long ) > dfs = [ & ]( TreeNode * node , long s ) -> int { if ( ! node ) return 0 ; s += node -> val ; int ans = cnt [ s - targetSum ]; ++ cnt [ s ]; ans += dfs ( node -> left , s ) + dfs ( node -> right , s ); -- cnt [ s ]; return ans ; }; return dfs ( root , 0 ); } };# 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 pathSum ( self , root : Optional [ TreeNode ], targetSum : int ) -> int : def dfs ( node , s ): if node is None : return 0 s += node . val ans = cnt [ s - targetSum ] cnt [ s ] += 1 ans += dfs ( node . left , s ) ans += dfs ( node . right , s ) cnt [ s ] -= 1 return ans cnt = Counter ({ 0 : 1 }) return dfs ( root , 0 )Video 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.