In the top-down style the parent passes data down to its children. Think of the recursion as a traveler carrying a backpack: the running sum, the path so far, the allowed range, the smallest and largest value seen. Use it whenever a node's answer depends on the path from the root to that node, not on its subtree.
The idea#
The state for a child is the parent's state plus the parent itself: dfs(node.left, curr_sum + node.val). Nothing comes back up except, perhaps, a global count. Each node is visited once, so the walk is as long as updating the state is .
The most useful version of this pattern is prefix sums on a tree path. It carries the array trick for subarray sums over to trees. A downward path from an ancestor a to a node x sums to prefix(x) - prefix(a), exactly as for a subarray. So at each node, the number of downward paths ending here with sum target is the number of ancestors whose prefix equals curr - target. A hashmap of prefix counts answers that in .
There is one catch, and it is easy to miss. The map must hold only the prefixes on the current root-to-node path. When the walk returns from a node, that node stops being an ancestor of whatever is visited next (its sibling's subtree). So you decrement its prefix on the way out. Without that, a node in the right subtree would pair with a prefix from the left subtree and count a "path" that doesn't exist.
How to spot it#
Look for an answer that depends on what lies above the node.
- "Root-to-leaf", "path from the root", "ancestor".
- A constraint that narrows as you go down: a valid BST range, a running min and max.
- "Count paths that sum to k" where paths go downward but may start anywhere.
- "Good nodes": nothing on the path above is larger.
Template#
This is the plain traveler. The state rides down as an argument. Nodes follow LeetCode's TreeNode(val, left, right).
walk(node, state):
if node is None: return
check node against state; update the answer
child_state = state updated with node # sum, range, min/max, path
walk(node.left, child_state)
walk(node.right, child_state)
walk(root, starting state)
Prefix sums on a path#
The counting version from "The idea" keeps the same walk. It adds a map of prefix counts and undoes each entry on the way out.
prefix = map of counts, with prefix[0] = 1 # a path may start at the root
walk(node, curr):
if node is None: return
curr += node.val
count += prefix[curr - target] # ancestors that start a path
prefix[curr] += 1
walk(node.left, curr)
walk(node.right, curr)
prefix[curr] -= 1 # leaving: no longer an ancestor
Brute force for comparison#
To see why the map is worth the extra lines, compare it with the obvious approach: start a fresh "sum from here" walk at every node. That is on a skewed tree. The prefix map gives .
Pitfalls#
These mistakes over-count or share state by accident.
- Forgetting to undo the prefix count. This is the easiest bug to make in the whole pattern:
prefix[curr] -= 1on the way out. Without it, sibling subtrees see each other's prefixes and you over-count. - The shared-reference bug. If you pass a mutable
pathlist down and append to it, every branch shares one list. Pass a copy (path + [node.val]), or append, recurse andpath.pop(). - Pruning on a negative tree. Stopping when
curr_sum > targetlooks safe, but if values can be negative the sum may come back down. You can't prune early. - Seeding the map. Without
prefix[0] = 1, paths that start at the root are missed. - Checking only the parent. A constraint set high up still applies deep down, so carry it all the way.
Problems in this pattern#
These problems use this pattern. Solve them before you open the hints below. Start with the first three.
- 112. Path Sum
- 113. Path Sum II
- 129. Sum Root to Leaf Numbers
- 98. Validate Binary Search Tree
- 1026. Maximum Difference Between Node and Ancestor
- 1372. Longest ZigZag Path in a Binary Tree
- 1448. Count Good Nodes in Binary Tree
- 437. Path Sum III
Hints#
Open a hint only after you have tried the problem for a while.
Hint: Path Sum
Carry (node, curr_sum) and compare with the target only at a leaf. A BFS queue of (node, curr_sum) also works and can return as soon as one leaf matches.
Hint: Path Sum II
Also carry the path, and copy it into the answer at a matching leaf. Watch the shared-reference bug: pass a copy, or append, recurse and pop.
Hint: Sum Root to Leaf Numbers
Carry the number itself: the child gets num * 10 + node.val. That is simpler than collecting digit strings. An empty tree sums to 0.
Hint: Validate Binary Search Tree
Carry the range a node may sit in. The root gets (-inf, +inf), a left child (lo, node.val) and a right child (node.val, hi). A deep node can satisfy its parent and still break a rule set higher up. An inorder walk that stays strictly increasing also works.
Hint: Maximum Difference Between Node and Ancestor
Carry the min and max seen on the path. At each node, the best difference is the larger of abs(node.val - lo) and abs(node.val - hi). Start both at root.val.
Hint: Longest ZigZag Path in a Binary Tree
Carry the direction you may take next and the current length. Going the allowed way extends the zigzag (length + 1). Going the other way restarts it at length 1, because that child begins a fresh zigzag. Record the maximum at every node.
Hint: Count Good Nodes in Binary Tree
Carry the largest value on the path above. A node is good when its value is at least that maximum. Pass the larger of the two down.
Hint: Path Sum III
Use the prefix-sum shape. Seed prefix[0] = 1, and decrement the count when you leave a node.
Key takeaways#
- Top-down means the parent passes state to its children as arguments.
- Use it when the answer depends on the path from the root, not on the subtree.
- Downward path sums are prefix differences, so a hashmap of prefix counts works on trees.
- Undo the prefix count when you leave a node; the map holds only the current path.
- Never share a mutable path between branches, and never prune early when values can be negative.