Binary Tree - DFS · Medium
LeetCode 437: Path Sum III — solution
Solve LeetCode 437 with prefix sums on the root-to-node path and a hash map of counts, undone on backtrack — O(n) time, O(n) space.
Problem in plain words
Count the downward paths in a binary tree whose node values add up to a given targetSum. A path can start at any node and end at any node below it, but it must always move from parent to child. It does not have to touch the root or end at a leaf.
Example: root 4, left child 2, and 2 has a left child 2. With target 4, the paths 4 (just the root) and 2 → 2 both qualify, so the answer is 2.
Intuition
This is the tree version of "count subarrays with sum k". While walking down one root-to-node path, keep the running sum curr. A downward path ending at the current node sums to target exactly when some earlier prefix on the same path had sum curr - target. So keep a hash map from prefix sum to how many times it appears on the current path. Look up curr - target, add the count to the answer, then insert curr before going deeper. When you return from a node, remove its prefix — it is no longer on the path.
Approach
- Create a map
seenwithseen[0] = 1— the empty prefix, so paths starting at the root are counted. - DFS with the running sum. At each node add
node.valto it. - Add
seen[curr - target]to the answer. - Increment
seen[curr], recurse into both children, then decrementseen[curr](backtrack). - Return the accumulated count.
Dry run
Tree 4 → left 2 → left 2, target 4.
| Node | curr | Look up curr − 4 | Found | seen after insert |
|---|---|---|---|---|
| 4 | 4 | 0 | 1 | {0:1, 4:1} |
| 2 | 6 | 2 | 0 | {0:1, 4:1, 6:1} |
| 2 | 8 | 4 | 1 | {0:1, 4:1, 6:1, 8:1} |
Total found = 2.
Java solution
class Solution {
private final Map<Long, Integer> seen = new HashMap<>();
private int target;
public int pathSum(TreeNode root, int targetSum) {
target = targetSum;
seen.put(0L, 1);
return dfs(root, 0L);
}
private int dfs(TreeNode node, long running) {
if (node == null) {
return 0;
}
running += node.val;
int found = seen.getOrDefault(running - target, 0);
seen.merge(running, 1, Integer::sum);
found += dfs(node.left, running) + dfs(node.right, running);
seen.merge(running, -1, Integer::sum);
return found;
}
}Python solution
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
seen = defaultdict(int)
seen[0] = 1
def dfs(node, running):
if node is None:
return 0
running += node.val
found = seen[running - targetSum]
seen[running] += 1
found += dfs(node.left, running) + dfs(node.right, running)
seen[running] -= 1
return found
return dfs(root, 0)Complexity
| Measure | Cost | Why |
|---|---|---|
| Time | O(n) | Each node does O(1) average hash-map work. |
| Space | O(n) | The map and recursion stack hold at most one path, which is n in the worst case. |
Edge cases and common mistakes
- Forgetting `seen[0] = 1` — without it, paths that start at the root are never counted.
- Integer overflow in Java — values up to 10^9 on a 1000-node path overflow
int. Keep prefix sums inlong. - Skipping the backtrack step — if you do not remove
currwhen leaving a node, prefixes from a sibling branch leak in and produce false matches. - Looking up after inserting — insert
curronly after the lookup, otherwise a target of0would count the empty path. - Empty tree — return
0.
›What is the brute-force approach?
From every node, start a fresh DFS that counts downward paths summing to the target. That is O(n·h) — O(n²) on a skewed tree. The prefix-sum map removes the inner DFS.
›Why must the map only contain the current path?
A valid path goes strictly downward, so its start must be an ancestor of its end. Prefixes from other branches are not ancestors and must not be matched.
