437. Path Sum III
Problem Description
You are given the root of a binary tree and an integer targetSum
. Your task is to find the number of paths in the tree where the sum of node values along the path equals targetSum
.
The path must follow these rules:
- It can start at any node in the tree (not necessarily the root)
- It can end at any node in the tree (not necessarily a leaf)
- It must go downwards, meaning you can only travel from parent nodes to child nodes
The solution uses a clever combination of prefix sums and hash tables. As we traverse the tree using DFS, we maintain a running sum s
from the root to the current node. The key insight is that if we want to find a path ending at the current node with sum equal to targetSum
, we need to check if there exists a prefix sum equal to s - targetSum
somewhere along the path from root to the current node.
The algorithm works as follows:
- We maintain a hash table
cnt
that stores the frequency of each prefix sum encountered along the current path - Initialize
cnt
with{0: 1}
to handle paths starting from the root - For each node, we calculate the prefix sum
s
and check how many timess - targetSum
appears in our hash table - this gives us the number of valid paths ending at the current node - We add the current prefix sum to the hash table before recursing to children
- After processing both children, we remove the current prefix sum from the hash table (backtracking) to ensure it doesn't affect other branches
The time complexity is O(n)
where n
is the number of nodes, as we visit each node once. The space complexity is O(h)
where h
is the height of the tree, needed for the recursion stack and the hash table storing at most h
prefix sums at any time.
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 parent nodes to child nodes.
DFS
- We arrive at DFS: Since we're dealing with a tree and need to explore paths from any node to any descendant node, DFS is the appropriate choice. DFS allows us to:
- Traverse the tree systematically
- Keep track of the current path from root to the current node
- Process each node while maintaining the context of its ancestors
- Backtrack efficiently when exploring different branches
Conclusion: The flowchart correctly leads us to use DFS (Depth-First Search) for Path Sum III. This makes sense because:
- We need to explore all possible paths in the tree
- Each path must go downward (parent to child), which naturally fits DFS traversal
- We need to maintain information about the current path (prefix sums), which DFS handles well with its recursive nature
- The backtracking aspect of DFS helps us efficiently manage the prefix sum counts when moving between different branches of the tree
Intuition
The key insight comes from recognizing that this is a subarray sum problem in disguise, just mapped onto a tree structure instead of an array.
Think about it this way: if we had an array and wanted to find all subarrays with sum equal to targetSum
, we could use the prefix sum technique. For any index j
, if we know the prefix sum up to j
is currentSum
, and somewhere earlier at index i
the prefix sum was currentSum - targetSum
, then the subarray from i+1
to j
has sum equal to targetSum
.
Now, how do we adapt this to a tree? In a tree, instead of a linear array, we have multiple paths from the root to various nodes. As we traverse down using DFS, we're essentially creating a temporary "array" - the path from root to the current node.
Here's the breakthrough: while traversing with DFS, we can treat the current path as if it were an array and apply the prefix sum technique.
For each node we visit:
- We maintain the cumulative sum from root to this node
- We check: "Have I seen a prefix sum of
currentSum - targetSum
anywhere along this path?" If yes, that means there's a valid path ending at the current node - We record our current prefix sum for future nodes to reference
The clever part is the backtracking - when we finish exploring a subtree and return to the parent, we need to "forget" the prefix sum we added. Why? Because when we explore the sibling subtree, that prefix sum from the first subtree shouldn't be available (they're on different paths from root).
This is why we increment cnt[s]
before recursing to children and decrement it after - we're essentially saying "this prefix sum is only valid for paths that go through the current node."
The initialization cnt = {0: 1}
handles the edge case where a path starting from the root itself has sum equal to targetSum
.
Solution Approach
Let's walk through the implementation step by step, building on our prefix sum intuition:
Data Structures Used:
- Hash Table (
Counter
): Stores the frequency of each prefix sum along the current path from root to the current node - Recursive DFS: Traverses the tree while maintaining the running sum
Implementation Details:
-
Initialize the Hash Table:
cnt = Counter({0: 1})
We start with
{0: 1}
to handle paths that start from the root and have sum equal totargetSum
. This is crucial - if we reach a node where the prefix sum equalstargetSum
, thens - targetSum = 0
, and we needcnt[0]
to be 1 to count this path. -
DFS Function Design:
def dfs(node, s):
node
: Current node being processeds
: Prefix sum from root to the parent of the current node- Returns: Number of valid paths in the subtree rooted at
node
-
Calculate Current Prefix Sum:
s += node.val
Update the prefix sum to include the current node's value.
-
Count Valid Paths Ending at Current Node:
ans = cnt[s - targetSum]
If there exists a prefix sum equal to
s - targetSum
somewhere along the path from root, then the subpath from that point to the current node has sum equal totargetSum
. The valuecnt[s - targetSum]
gives us the count of all such valid paths. -
Update Hash Table Before Recursion:
cnt[s] += 1
Add the current prefix sum to our hash table so that nodes in the subtree can use it to find valid paths.
-
Recursive Exploration:
ans += dfs(node.left, s) ans += dfs(node.right, s)
Recursively explore left and right subtrees, accumulating the count of valid paths from each.
-
Backtrack - Remove Current Prefix Sum:
cnt[s] -= 1
This is the crucial backtracking step. Once we've finished exploring the current node's subtree, we remove its prefix sum from the hash table. This ensures that when we explore other branches of the tree, they won't incorrectly use this prefix sum (since they're on different paths from root).
Why This Works:
The algorithm maintains an invariant: at any point during the traversal, cnt
contains exactly the prefix sums along the current path from root to the current node. This allows us to correctly identify all valid subpaths ending at each node while avoiding false positives from other branches.
Time and Space Complexity:
- Time:
O(n)
wheren
is the number of nodes, as we visit each node exactly once - Space:
O(h)
whereh
is the height of the tree, needed for:- Recursion stack:
O(h)
- Hash table: At most
O(h)
entries (one per level in the current path)
- Recursion stack:
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to see how the prefix sum approach works on a binary tree.
Consider this binary tree with targetSum = 8
:
10 / \ 5 -3 / \ \ 3 2 11 / \ \ 3 -2 1
Let's trace through the DFS traversal, focusing on how we find paths summing to 8:
Initial State:
cnt = {0: 1}
(to handle paths starting from root)- Start DFS at root (10)
Step 1: Visit node 10
- Prefix sum
s = 0 + 10 = 10
- Check:
cnt[10 - 8] = cnt[2] = 0
(no paths ending here) - Update:
cnt = {0: 1, 10: 1}
- Recurse to left child (5)
Step 2: Visit node 5
- Prefix sum
s = 10 + 5 = 15
- Check:
cnt[15 - 8] = cnt[7] = 0
(no paths ending here) - Update:
cnt = {0: 1, 10: 1, 15: 1}
- Recurse to left child (3)
Step 3: Visit node 3
- Prefix sum
s = 15 + 3 = 18
- Check:
cnt[18 - 8] = cnt[10] = 1
✓ Found path: 5→3 = 8 - Update:
cnt = {0: 1, 10: 1, 15: 1, 18: 1}
- Recurse to left child (3)
Step 4: Visit node 3 (leaf)
- Prefix sum
s = 18 + 3 = 21
- Check:
cnt[21 - 8] = cnt[13] = 0
(no paths ending here) - Update:
cnt = {0: 1, 10: 1, 15: 1, 18: 1, 21: 1}
- No children, backtrack:
cnt[21] -= 1
Step 5: Back at node 3, visit right child (-2)
- Prefix sum
s = 18 + (-2) = 16
- Check:
cnt[16 - 8] = cnt[8] = 0
(no paths ending here) - Update:
cnt = {0: 1, 10: 1, 15: 1, 18: 1, 16: 1}
- No children, backtrack:
cnt[16] -= 1
Step 6: Back at node 3, finished both children
- Backtrack:
cnt[18] -= 1
cnt = {0: 1, 10: 1, 15: 1}
Step 7: Back at node 5, visit right child (2)
- Prefix sum
s = 15 + 2 = 17
- Check:
cnt[17 - 8] = cnt[9] = 0
(no paths ending here) - Update:
cnt = {0: 1, 10: 1, 15: 1, 17: 1}
- Recurse to right child (1)
Step 8: Visit node 1
- Prefix sum
s = 17 + 1 = 18
- Check:
cnt[18 - 8] = cnt[10] = 1
✓ Found path: 5→2→1 = 8 - Update:
cnt = {0: 1, 10: 1, 15: 1, 17: 1, 18: 1}
- No children, backtrack:
cnt[18] -= 1
Continuing the traversal...
The process continues similarly for the right subtree. Notice how:
- When we backtrack from node 5, we remove its prefix sum (15) from
cnt
- This ensures that when we explore node -3's subtree, we won't incorrectly use the prefix sum from node 5's path
Key Observations:
- The hash table
cnt
always contains only the prefix sums along the current path from root - When we found the path 5→3, it was because at node 3 (with prefix sum 18), we found that
cnt[10] = 1
, meaning there was a node with prefix sum 10 earlier in the path - The difference 18 - 10 = 8 equals our target, confirming the path from after node 10 to node 3 sums to 8
- Backtracking ensures we don't mix up paths from different branches
In total, this example would find paths like:
- 5 → 3 (sum = 8)
- 5 → 2 → 1 (sum = 8)
- -3 → 11 (sum = 8)
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
9from collections import Counter
10
11class Solution:
12 def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
13 """
14 Find the number of paths in a binary tree that sum to targetSum.
15 Paths can start and end at any nodes but must go downward.
16
17 Args:
18 root: Root node of the binary tree
19 targetSum: Target sum to find
20
21 Returns:
22 Number of paths that sum to targetSum
23 """
24
25 def dfs(node: Optional[TreeNode], current_sum: int) -> int:
26 """
27 Depth-first search to count valid paths using prefix sum technique.
28
29 Args:
30 node: Current node being processed
31 current_sum: Cumulative sum from root to parent of current node
32
33 Returns:
34 Number of valid paths that include the current node
35 """
36 # Base case: empty node contributes no paths
37 if node is None:
38 return 0
39
40 # Update cumulative sum to include current node
41 current_sum += node.val
42
43 # Count paths ending at current node with sum equal to targetSum
44 # We look for (current_sum - targetSum) in our prefix sum counter
45 # If it exists, those occurrences represent valid starting points
46 path_count = prefix_sum_count[current_sum - targetSum]
47
48 # Add current prefix sum to counter for use by descendant nodes
49 prefix_sum_count[current_sum] += 1
50
51 # Recursively count paths in left and right subtrees
52 path_count += dfs(node.left, current_sum)
53 path_count += dfs(node.right, current_sum)
54
55 # Backtrack: remove current sum from counter as we return up the tree
56 # This ensures the counter only contains sums from the current path
57 prefix_sum_count[current_sum] -= 1
58
59 return path_count
60
61 # Initialize prefix sum counter with 0:1 to handle paths starting from root
62 prefix_sum_count = Counter({0: 1})
63
64 # Start DFS from root with initial sum of 0
65 return dfs(root, 0)
66
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 // HashMap to store the frequency of prefix sums
18 // Key: prefix sum, Value: count of paths with this sum
19 private Map<Long, Integer> prefixSumCount = new HashMap<>();
20 private int targetSum;
21
22 /**
23 * Finds the number of paths in a binary tree that sum to targetSum.
24 * Paths can start and end at any node (must go downwards).
25 *
26 * @param root The root of the binary tree
27 * @param targetSum The target sum to find
28 * @return The number of paths that sum to targetSum
29 */
30 public int pathSum(TreeNode root, int targetSum) {
31 // Initialize with prefix sum 0 having count 1
32 // This handles paths starting from the root
33 prefixSumCount.put(0L, 1);
34 this.targetSum = targetSum;
35
36 // Start DFS traversal from root with initial sum 0
37 return dfs(root, 0);
38 }
39
40 /**
41 * Performs depth-first search to count paths with the target sum.
42 * Uses prefix sum technique to find paths efficiently.
43 *
44 * @param node Current node being processed
45 * @param currentPrefixSum Sum of all nodes from root to current node's parent
46 * @return Number of valid paths in the subtree rooted at this node
47 */
48 private int dfs(TreeNode node, long currentPrefixSum) {
49 // Base case: null node contributes 0 paths
50 if (node == null) {
51 return 0;
52 }
53
54 // Update prefix sum to include current node
55 currentPrefixSum += node.val;
56
57 // Check if there exists a prefix sum such that
58 // currentPrefixSum - prefixSum = targetSum
59 // This means the path from that prefix to current node sums to targetSum
60 int pathCount = prefixSumCount.getOrDefault(currentPrefixSum - targetSum, 0);
61
62 // Add current prefix sum to the map for use by descendant nodes
63 prefixSumCount.merge(currentPrefixSum, 1, Integer::sum);
64
65 // Recursively explore left and right subtrees
66 pathCount += dfs(node.left, currentPrefixSum);
67 pathCount += dfs(node.right, currentPrefixSum);
68
69 // Backtrack: remove current prefix sum from map
70 // This ensures the prefix sum is only available to current node's descendants
71 prefixSumCount.merge(currentPrefixSum, -1, Integer::sum);
72
73 return pathCount;
74 }
75}
76
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 pathSum(TreeNode* root, int targetSum) {
15 // Map to store prefix sum frequencies
16 // Key: prefix sum value, Value: count of occurrences
17 unordered_map<long long, int> prefixSumCount;
18
19 // Initialize with 0 sum having count 1 (empty path)
20 // This handles cases where a path from root equals targetSum
21 prefixSumCount[0] = 1;
22
23 // Define recursive DFS function using lambda with explicit this capture
24 auto depthFirstSearch = [&](this auto&& depthFirstSearch,
25 TreeNode* currentNode,
26 long long currentPrefixSum) -> int {
27 // Base case: if node is null, no paths found
28 if (!currentNode) {
29 return 0;
30 }
31
32 // Update current prefix sum by adding current node's value
33 currentPrefixSum += currentNode->val;
34
35 // Check if there exists a previous prefix sum such that
36 // currentPrefixSum - previousPrefixSum = targetSum
37 // This means: previousPrefixSum = currentPrefixSum - targetSum
38 int pathCount = prefixSumCount[currentPrefixSum - targetSum];
39
40 // Add current prefix sum to the map for use by descendant nodes
41 ++prefixSumCount[currentPrefixSum];
42
43 // Recursively search left and right subtrees
44 pathCount += depthFirstSearch(currentNode->left, currentPrefixSum);
45 pathCount += depthFirstSearch(currentNode->right, currentPrefixSum);
46
47 // Backtrack: remove current prefix sum from map
48 // This ensures it's not available to nodes in other branches
49 --prefixSumCount[currentPrefixSum];
50
51 return pathCount;
52 };
53
54 // Start DFS from root with initial prefix sum of 0
55 return depthFirstSearch(root, 0);
56 }
57};
58
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 * Counts the number of paths in a binary tree that sum to targetSum.
17 * Paths can start and end at any nodes but must go downward (parent to child).
18 *
19 * @param root - The root node of the binary tree
20 * @param targetSum - The target sum to find paths for
21 * @returns The number of paths that sum to targetSum
22 */
23function pathSum(root: TreeNode | null, targetSum: number): number {
24 // Map to store prefix sums and their frequencies
25 // Key: prefix sum value, Value: count of paths with this sum
26 const prefixSumCount: Map<number, number> = new Map();
27
28 /**
29 * Performs depth-first search to find all paths with the target sum.
30 * Uses prefix sum technique to efficiently count valid paths.
31 *
32 * @param node - Current node being processed
33 * @param currentSum - Sum of values from root to current node
34 * @returns Number of valid paths found in the subtree
35 */
36 const dfs = (node: TreeNode | null, currentSum: number): number => {
37 // Base case: if node is null, no paths exist
38 if (!node) {
39 return 0;
40 }
41
42 // Update current path sum by adding current node's value
43 currentSum += node.val;
44
45 // Check if there exists a prefix sum such that
46 // currentSum - prefixSum = targetSum
47 // This means the path from that prefix to current node equals targetSum
48 let pathCount: number = prefixSumCount.get(currentSum - targetSum) ?? 0;
49
50 // Add current prefix sum to the map for use by descendant nodes
51 prefixSumCount.set(currentSum, (prefixSumCount.get(currentSum) ?? 0) + 1);
52
53 // Recursively search left and right subtrees
54 pathCount += dfs(node.left, currentSum);
55 pathCount += dfs(node.right, currentSum);
56
57 // Backtrack: remove current prefix sum from map
58 // This ensures the prefix sum is only available to descendant nodes
59 prefixSumCount.set(currentSum, (prefixSumCount.get(currentSum) ?? 0) - 1);
60
61 return pathCount;
62 };
63
64 // Initialize with prefix sum 0 to handle paths starting from root
65 prefixSumCount.set(0, 1);
66
67 // Start DFS traversal from root with initial sum of 0
68 return dfs(root, 0);
69}
70
Time and Space Complexity
Time Complexity: O(n)
The algorithm performs a depth-first search (DFS) traversal of the binary tree. Each node is visited exactly once during the traversal. At each node, the following operations are performed:
- Adding the node's value to the current sum:
O(1)
- Looking up
cnt[s - targetSum]
:O(1)
average case for dictionary lookup - Updating
cnt[s]
by incrementing:O(1)
average case for dictionary update - Recursive calls to left and right children
- Decrementing
cnt[s]
:O(1)
average case for dictionary update
Since each of the n
nodes is visited once and performs O(1)
operations, the overall time complexity is O(n)
.
Space Complexity: O(n)
The space complexity consists of:
- Recursion call stack: In the worst case (skewed tree), the depth of recursion can be
O(n)
. In a balanced tree, it would beO(log n)
. - HashMap (Counter)
cnt
: In the worst case, the HashMap stores a unique prefix sum for each node along the current path from root to leaf. The maximum size would be the height of the tree, which isO(n)
in the worst case.
Taking the worst-case scenario where the tree is completely unbalanced (like a linked list), both the recursion stack and the HashMap can grow to O(n)
, resulting in an overall space complexity of O(n)
.
Common Pitfalls
1. Forgetting to Initialize the Hash Table with {0: 1}
The Pitfall:
Many people forget to initialize the hash table with {0: 1}
and start with an empty hash table instead. This causes the algorithm to miss paths that start from the root node itself.
Why It Happens:
When a path from the root to the current node has a sum exactly equal to targetSum
, we need to check for current_sum - targetSum = 0
in our hash table. Without the initial {0: 1}
, these valid paths won't be counted.
Example:
Tree: 10 / \ 5 -3 / \ \ 3 2 11 / \ \ 3 -2 1 targetSum = 8
The path 10 -> -3 -> 11
sums to 18. When at node 11, current_sum = 18
. We check for 18 - 8 = 10
in our hash table. But also, the path 10 -> 5 -> 3
sums to 18, and when at the second 3, current_sum = 18
. Without {0: 1}
, we'd miss counting the path starting from root.
Incorrect Code:
# Wrong initialization prefix_sum_count = Counter() # Missing {0: 1}
Correct Code:
# Correct initialization prefix_sum_count = Counter({0: 1})
2. Forgetting to Backtrack (Remove Prefix Sum After Processing)
The Pitfall: Failing to remove the current prefix sum from the hash table after processing both children leads to incorrect counts. The hash table would contain prefix sums from other branches, causing false positives.
Why It Happens: The hash table should only contain prefix sums along the current path from root to the current node. If we don't remove a prefix sum after processing a subtree, it remains available for nodes in completely different branches.
Example:
Tree: 1 / \ 2 3 / 4 targetSum = 3
When processing node 4, the prefix sum is 7 (1+2+4). If we don't remove this from the hash table before processing node 3, node 3 might incorrectly use this prefix sum even though nodes 2 and 4 are not on the path to node 3.
Incorrect Code:
def dfs(node, current_sum):
if node is None:
return 0
current_sum += node.val
path_count = prefix_sum_count[current_sum - targetSum]
prefix_sum_count[current_sum] += 1
path_count += dfs(node.left, current_sum)
path_count += dfs(node.right, current_sum)
# Missing backtracking step!
return path_count
Correct Code:
def dfs(node, current_sum):
if node is None:
return 0
current_sum += node.val
path_count = prefix_sum_count[current_sum - targetSum]
prefix_sum_count[current_sum] += 1
path_count += dfs(node.left, current_sum)
path_count += dfs(node.right, current_sum)
# Critical backtracking step
prefix_sum_count[current_sum] -= 1
return path_count
3. Using the Hash Table After Backtracking
The Pitfall: Some implementations mistakenly try to use the prefix sum after it has been removed from the hash table during backtracking.
Why It Happens: Confusion about the order of operations - the prefix sum must be removed only after all computations for the current node and its subtrees are complete.
Incorrect Code:
def dfs(node, current_sum):
if node is None:
return 0
current_sum += node.val
prefix_sum_count[current_sum] += 1
# Backtracking too early!
prefix_sum_count[current_sum] -= 1
# This will always be 0 because we just removed the current sum
path_count = prefix_sum_count[current_sum - targetSum]
path_count += dfs(node.left, current_sum)
path_count += dfs(node.right, current_sum)
return path_count
Correct Order:
- Update current sum
- Count valid paths ending at current node
- Add current sum to hash table
- Process children recursively
- Remove current sum from hash table (backtrack)
- Return the total count
Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?
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!