Tree DFS Paths and Sums
Tree BFS answers questions about levels. Tree DFS answers questions about paths and subtrees — and the reason is structural: recursion naturally gives you two channels of information that BFS does not.
Down the recursion you carry context: the path so far, the remaining target, the accumulated number. Up the recursion you return summaries: a height, a subtree sum, a best-so-far.
Almost every tree DFS problem is a question of which channel you need. Get that right and the code writes itself.
What you’ll learn
- The two information channels, and how to decide which a problem wants.
- The choose / recurse / un-choose discipline for collecting paths, and why you must copy the path when you record it.
- The split-brain trick: when the value you return differs from the value you record. This is the single most reusable idea on the page.
- Why a node with one child is not a leaf, and where that bites.
- Three real LeetCode problems solved in the browser: 112, 113, 543.
The cue
Channel 1 — carry state downward
Pass what the child needs as an argument. For path problems that is usually the remaining target, which is cleaner than accumulating a running total and comparing at the bottom.
def has_path_sum(root, target):
if not root:
return False
if not root.left and not root.right: # a real leaf
return target == root.val
remaining = target - root.val
return (has_path_sum(root.left, remaining)
or has_path_sum(root.right, remaining))def has_path_sum(root, target):
if not root:
return False
if not root.left and not root.right: # a real leaf
return target == root.val
remaining = target - root.val
return (has_path_sum(root.left, remaining)
or has_path_sum(root.right, remaining))Channel 2 — return summaries upward
When the answer depends on children, compute both sides first, then combine.
def max_depth(root):
if not root:
return 0
return 1 + max(max_depth(root.left), max_depth(root.right))def max_depth(root):
if not root:
return 0
return 1 + max(max_depth(root.left), max_depth(root.right))That is the whole shape: base case for the empty tree, recurse both sides,
combine. 9898 (validate BST), 110110 (balanced), and 104104 are all this with a
different combine step.
Collecting paths — choose / recurse / un-choose
To record paths rather than just test them, maintain one shared list and undo your change on the way out. This is backtracking applied to a tree.
def path_sum(root, target):
out, path = [], []
def dfs(node, remaining):
if not node:
return
path.append(node.val) # choose
if not node.left and not node.right and remaining == node.val:
out.append(list(path)) # COPY, not path
else:
dfs(node.left, remaining - node.val)
dfs(node.right, remaining - node.val)
path.pop() # un-choose
dfs(root, target)
return outdef path_sum(root, target):
out, path = [], []
def dfs(node, remaining):
if not node:
return
path.append(node.val) # choose
if not node.left and not node.right and remaining == node.val:
out.append(list(path)) # COPY, not path
else:
dfs(node.left, remaining - node.val)
dfs(node.right, remaining - node.val)
path.pop() # un-choose
dfs(root, target)
return outThe split-brain trick
Here is the idea worth taking away. Some problems ask about paths that do not pass through the root — diameter, maximum path sum. The trick is that the recursion returns one thing while a side channel records another:
- Return the best path that goes downward from this node, because that is all a parent can extend.
- Record the best path that bends at this node (left side + right side), because that path is complete and cannot be extended upward.
def diameter(root):
best = 0
def height(node):
nonlocal best
if not node:
return 0
left = height(node.left)
right = height(node.right)
best = max(best, left + right) # RECORD: path bending here
return 1 + max(left, right) # RETURN: extendable downward path
height(root)
return bestdef diameter(root):
best = 0
def height(node):
nonlocal best
if not node:
return 0
left = height(node.left)
right = height(node.right)
best = max(best, left + right) # RECORD: path bending here
return 1 + max(left, right) # RETURN: extendable downward path
height(root)
return bestThose two lines are different on purpose. left + rightleft + right is a path through
this node that is finished. 1 + max(left, right)1 + max(left, right) is what the parent can
build on. Conflating them — returning left + rightleft + right — produces nonsense,
because a bent path cannot be extended by a parent.
This exact structure solves LC 543, LC 124, LC 687, LC 1522 and more. Once you recognise “any path, not necessarily through the root”, you write these two lines and fill in the details.
| Time | Space | |
|---|---|---|
| Any single-pass tree DFS | recursion, hh = height |
Space is for a balanced tree and for a degenerate one — the mirror image of BFS, which is in the widest level.
The variant map
| Variant | Channel used | Canonical problem |
|---|---|---|
| Does a path exist? | Down (remaining target) | 112 Path Sum |
| Collect all paths | Down + backtracking | 113 · 257 |
| Build a number down the path | Down (total * 10 + valtotal * 10 + val) | 129 Sum Root to Leaf |
| Height / depth / balanced | Up (summary) | 104 · 110 |
| Any path, may bend | Split-brain | 543 · 124 · 687 |
| Count paths summing to k | Down, with a prefix-sum map | 437 |
| Validate a subtree property | Up, or down with bounds | 98 · 100 · 572 |
Practice — real LeetCode problems
LC 112 — Path Sum · Easy
Problem. Given the rootroot of a binary tree and an integer targetSumtargetSum,
return TrueTrue if there is a root-to-leaf path whose values sum to
targetSumtargetSum.
Constraints. 0 <= number of nodes <= 50000 <= number of nodes <= 5000,
-1000 <= Node.val <= 1000-1000 <= Node.val <= 1000.
Examples. [5,4,8,11,null,13,4,7,2,null,null,null,1][5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22targetSum = 22
gives TrueTrue (5 -> 4 -> 11 -> 25 -> 4 -> 11 -> 2) · [1,2,3][1,2,3], targetSum = 5targetSum = 5 gives FalseFalse
· [][], targetSum = 0targetSum = 0 gives FalseFalse
Editorial — approach, complexity, follow-ups
Subtract as you descend and test at the leaves. Short-circuiting oror means
the search stops at the first success.
Time . Space .
The two test cases that matter:
([], 0)([], 0)givesFalseFalse. An empty tree has no root-to-leaf path at all, so even a target of00fails. A solution returningTrueTruefortarget == 0target == 0gets this wrong.([1, 2], 1)([1, 2], 1)givesFalseFalse. Node 1 has only a left child, so the sole path is1 -> 21 -> 2summing to 3. This is the one-child trap described above.
Follow-ups you should expect: “Return all such paths?” — LC 113, next.
“Count paths that need not start at the root?” — LC 437: carry a prefix-sum
CounterCounter down and decrement it on the way back up, so only the current
root-to-node path is in scope. “Do it iteratively?” — an explicit stack of
(node, remaining)(node, remaining) pairs. “What about negative values?” — fully supported
here; note that they rule out any early-exit pruning like “remaining is
already negative, give up”.
LC 113 — Path Sum II · Medium
Problem. Return all root-to-leaf paths whose values sum to
targetSumtargetSum. Each path is returned as a list of node values.
Constraints. 0 <= number of nodes <= 50000 <= number of nodes <= 5000,
-1000 <= Node.val <= 1000-1000 <= Node.val <= 1000.
Examples. [5,4,8,11,null,13,4,7,2,null,null,5,1][5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22targetSum = 22
gives [[5,4,11,2],[5,8,4,5]][[5,4,11,2],[5,8,4,5]] · [1,2,3][1,2,3], targetSum = 5targetSum = 5 gives [][]
Editorial — approach, complexity, follow-ups
Backtracking on a tree. The shared pathpath list always holds the route from the
root to the current node; appending on entry and popping on exit keeps that
invariant automatically.
Time in the worst case — to visit every node, plus
to copy each recorded path. Space for the recursion and
pathpath, plus the output.
Two disciplines make this correct:
- Copy on record.
list(path)list(path)— see the caution above. - One
poppopperappendappend, on every branch. Here thepop()pop()is after theif/elseif/else, so it executes whether or not a path was recorded. Putting areturnreturninside theififbranch without popping first is a classic leak that corrupts every later path.
Note the elseelse: once a matching leaf is found there are no children to
recurse into, so the branch is genuinely exclusive. Recursing anyway would
still work (both children are NoneNone) but the elseelse states the intent.
Follow-ups you should expect: “Return the paths as strings (LC 257)?” —
same structure, join with "->""->". “Paths that need not end at a leaf?” —
record at every node instead of only at leaves. “Count instead of collect
(LC 437)?” — prefix-sum map with an undo on the way back up; far better than
re-walking. “Why not build a new list per recursive call?” — you can
(dfs(node.left, remaining, path + [node.val])dfs(node.left, remaining, path + [node.val])) and it removes the copy bug,
but it allocates per node instead of once per result.
LC 543 — Diameter of Binary Tree · Easy
Problem. The diameter is the length of the longest path between any two nodes, measured in edges. The path may or may not pass through the root.
Constraints. 1 <= number of nodes <= 10^41 <= number of nodes <= 10^4,
-100 <= Node.val <= 100-100 <= Node.val <= 100.
Examples. [1,2,3,4,5][1,2,3,4,5] gives 33 (the path 4 -> 2 -> 1 -> 34 -> 2 -> 1 -> 3) ·
[1,2][1,2] gives 11 · [1][1] gives 00
Editorial — approach, complexity, follow-ups
The longest path in a tree bends at exactly one node — its highest point. So
consider each node as that apex: the best path bending there is
height(left) + height(right)height(left) + height(right) edges. Take the maximum over all nodes.
Meanwhile the recursion must return 1 + max(left, right)1 + max(left, right), because a parent
can only extend a path that runs straight down.
Time — one visit per node. Space .
The naive alternative — compute heightheight separately at every node — is
because heights get recomputed. Threading the recording through the
same single traversal is what makes it linear, and explaining that is the
point of the problem.
Follow-ups you should expect: “Maximum path sum (LC 124)?” — same
skeleton, but clamp negative contributions with
max(0, height(child))max(0, height(child)) since you may drop a negative branch entirely, and
record node.val + left + rightnode.val + left + right. “Longest path with equal values (LC 687)?”
— only extend into a child whose value matches. “Return the path itself?” —
track the apex node and re-descend from it. “Nodes up to in a chain?” —
raise the recursion limit or go iterative.
LeetCode problem set
| # | Problem | Difficulty | The twist |
|---|---|---|---|
| 104 | Maximum Depth of Binary Tree | Easy | The plain upward summary: 1 + max(left, right)1 + max(left, right) |
| 112 | Path Sum | Easy | Carry the remaining target down; a one-child node is not a leaf |
| 543 | Diameter of Binary Tree | Easy | Split-brain: return the height, record left + rightleft + right |
| 110 | Balanced Binary Tree | Easy | Return the height, or a sentinel -1-1 to abort early |
| 113 | Path Sum II | Medium | Backtracking; copy the path when recording |
| 129 | Sum Root to Leaf Numbers | Medium | Carry total * 10 + valtotal * 10 + val downward |
| 437 | Path Sum III | Medium | Prefix-sum map carried down, decremented on the way back up |
| 124 | Binary Tree Maximum Path Sum | Hard | Split-brain with negative branches clamped to 00 |
Interview follow-ups
| They ask | What they’re checking | The answer |
|---|---|---|
| “DFS or BFS here?” | Deliberate choice | Paths and subtree properties are DFS; per-level and nearest-thing are BFS |
| “Why return one value and record another?” | The split-brain insight | A parent can only extend a straight-down path; a bent path is already complete |
| “Why copy the path?” | Reference vs. value | pathpath is mutated throughout; storing it stores an alias that later pops empty |
| “Space complexity?” | Precision | — balanced, degenerate; the mirror of BFS’s |
| “What if the tree is 10^4 deep?” | Practical Python | RecursionErrorRecursionError at the default limit of 1000; raise it or use an explicit stack |
| “Count paths not starting at the root?” | Composition | Prefix-sum CounterCounter down the path, decremented on unwind (LC 437) |
| “Can you avoid recomputing heights?” | Complexity awareness | Thread the recording through one traversal; separate height calls make it |
Edge-case checklist
- Empty tree — LC 112 must return
FalseFalseeven fortargetSum = 0targetSum = 0. - Single node — diameter
00; a path sum equal to that node’s value isTrueTrue. - One-child nodes —
[1,2][1,2]with target11isFalseFalse. The defining trap of this family. - Negative node values — legal; they forbid pruning on “the remaining
target went negative”, and they force the
max(0, ...)max(0, ...)clamp in LC 124. - Target sum of zero — valid; do not treat
00as “no target”. - Deep degenerate tree — recursion limit; mention it before it bites.
- Duplicate values — never identify nodes by value; use the node objects.
- Path bending away from the root —
[1,2,3,4,null,null,5,6,null,null,7][1,2,3,4,null,null,5,6,null,null,7]has diameter66, and no path through the root achieves it.
Recap
- Tree DFS gives you two channels: carry context down as arguments, return summaries up. Deciding which the problem needs is most of the work.
- A leaf has neither child. Using
if not nodeif not nodeas the leaf test wrongly accepts the missing side of a one-child node — the single most common bug here. - To collect paths, choose / recurse / un-choose, and record
list(path)list(path), neverpathpath. Everyappendappendneeds exactly onepoppop, on every branch. - The split-brain trick — return
1 + max(left, right)1 + max(left, right), recordleft + rightleft + right— solves the whole “any path, may bend” family in two lines. - time and space; watch Python’s 1000-frame recursion limit on degenerate trees.
- DFS composes with other patterns: LC 437 is this page plus prefix sums and a hash map.
Next: BST Patterns — what changes once the tree is ordered, and why an in-order walk is often the entire solution.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
