DSA sheet · Trees · DFS pattern

Path Sum

This is the first root-to-leaf path problem in the sheet. The teacher uses it to teach three things: how to read the constraints for overflow, how to choose between DFS and BFS by thinking about best and worst cases, and a neat trick: instead of adding up a running sum, subtract each node from the target as you go down. She solves it with DFS first, then BFS. Path Sum II (next problem) builds directly on this one.

Every problem below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the conditions from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

Part A · Path Sum with DFS

LeetCode 112

1The question in simple words

You get the root of a binary tree and a number targetSum. Return True if there is at least one root-to-leaf path whose node values add up to exactly targetSum. Otherwise return False.

            5
           / \
          4   8
         /   / \
       11   13  4
       / \       \
      7   2       1          targetSum = 22

This tree has 4 root-to-leaf paths (one per leaf):

pathsum= 22?
5 → 4 → 11 → 727no
5 → 4 → 11 → 222yes
5 → 8 → 1326no
5 → 8 → 4 → 118no

One path works, so the answer is True. We only need to know whether such a path exists, not which one.

2What the constraints tell us

Doubt: in the video she also mentions "up to 10⁸ is safe, beyond that TLE" while doing this sum. Is the sum about time?
→ No, two different checks got mixed together there. The sum 5 × 10⁶ is about overflow: will the number fit in an int? (Yes.) The 10⁸ rule is about time: how many steps can the code take? Our code takes about n = 5000 steps, which is tiny. Both checks pass.

3Intuition: DFS or BFS?

Before writing code, the teacher asks: which traversal fits this problem better? She compares the best and worst cases.

DFSBFS
worst case: the answer is the last path (or none)visits all nodesvisits all nodes
best case: the answer is the first, left-most pathreturns True after one path, never touches the right side of the rootstill has to go through the upper levels of every path first

Worst cases are equal, but DFS has a better best case. And the problem is literally about paths from top to bottom, which is how DFS moves. So she goes with DFS.

The mental picture

Think of targetSum as a budget you carry down the tree. At each node you spend that node's value. When you reach a leaf, you want to have spent the budget exactly.

4Building the logic from the example

What does each DFS call need?

Two ways to track the sum (the teacher shows both)

Way 1: running sumWay 2: shrinking target
ideaStart s = 0. Add each node's value as you go down.Start with targetSum. Subtract each node's value as you go down.
at a leaf, the path is good when…s == targetSumwhat's left becomes 0
extra variable?yes (s)no, we reuse targetSum

She uses Way 2 and walks it on the example. Start at 5 needing 22. We found 5, so below 5 we only need 17. At 4 we found 4, so below it we need 13. At 11 we found 11, so below it we need 2.

Rule 1: the base case, root is None → False

The teacher asks a tricky question first: if the node is None and targetSum is 0, should we return True? It's tempting: "nothing left to find, nothing here, so it matches."

No. A target of 0 is a perfectly normal target. Values can be negative, so a real path like 5 → −5 adds up to 0. But an empty spot isn't a path at all. No node, no path, so the answer is False, whatever the target is.

Rule 1if root is None: return False
So an empty tree with target 0 gives False, which is what LeetCode expects.

Rule 2: only judge the sum at a leaf

The question says root-to-leaf. We may only say "yes, this works" when we stand on a leaf. A leaf is a node where root.left is None and root.right is None.

Her first draft at a leaf was "return True if targetSum is 0". The dry run showed a problem with that. When the call for leaf 7 starts, the target it receives is 2. We haven't subtracted 7 yet, because the subtraction happens only when we pass the value down to the children. So at the leaf, the right check is "is the remaining target equal to this leaf's value?", i.e. targetSum == root.val.

Doubt 1: so is targetSum == 0 just wrong?
→ In this code shape, yes. 7 would compare 2 with 0, and leaf 2 would also compare 2 with 0 and wrongly fail. targetSum == root.val means the same thing as targetSum − root.val == 0: "after spending this last node, the budget is exactly used up". The teacher caught this on screen during her dry run and fixed it.
Rule 2if root.left is None and root.right is None: return targetSum == root.val

Rule 3: not a leaf → ask the children, with a smaller target

The good path, if there is one, continues either through the left child or through the right child. So we ask both. What target do we send them? Not 22 again, because 5 is already part of every path below 5. We send targetSum − root.val = 17.

Doubt 2: or or and?
→ We need any one path to work, not all of them. If the left side finds one, we're done → or. Python's or also skips the right call when the left is already True. In our example, that means the whole right half under 8 is never visited.
Doubt 3: why is Rule 1 needed if Rule 2 catches every leaf?
→ A node with one child isn't a leaf, so we call both sides, and one of them is None. Example: 4 (under 5) has only a left child 11. Its right call gets None, and Rule 1 answers False for it. That's correct: None is not a path. Rule 1 also handles the empty tree.

5Approach steps

  1. If the node is None → return False.
  2. If the node is a leaf → return whether the remaining target equals its value.
  3. Otherwise subtract the node's value from the target, and ask the left child or the right child with the new target.

6Code (Python)

Path Sum with DFS (shrinking target, the teacher's way)
class Solution:
    def hasPathSum(self, root, targetSum):
        if root is None:                                   # Rule 1: no node, no path
            return False
        if root.left is None and root.right is None:       # Rule 2: leaf
            return targetSum == root.val
        remaining = targetSum - root.val                   # spend this node's value
        return (self.hasPathSum(root.left, remaining) or   # Rule 3: either side
                self.hasPathSum(root.right, remaining))

At the end of the video, the teacher leaves you a task: solve it with Way 1 (a running sum) too. Here it is. Only the bookkeeping changes.

Path Sum with DFS (running sum, the homework version)
class Solution:
    def hasPathSum(self, root, targetSum):
        return self.dfs(root, 0, targetSum)

    def dfs(self, node, s, targetSum):
        if node is None:
            return False
        s += node.val                                      # add instead of subtract
        if node.left is None and node.right is None:
            return s == targetSum                          # whole path collected
        return self.dfs(node.left, s, targetSum) or self.dfs(node.right, s, targetSum)

7Code line by line

linewhat it means
if root is None: return FalseBase case. Stops the recursion below leaves and below one-child nodes. Also handles an empty tree, even when the target is 0.
if root.left is None and root.right is None:"Am I standing on a leaf?" Both children must be missing.
return targetSum == root.valAt a leaf, the path ends. It's good only if this last value uses up exactly what's left.
remaining = targetSum - root.valThis node is now part of the path, so the children need to find less.
self.hasPathSum(root.left, remaining)Look for a good path through the left child first.
or self.hasPathSum(root.right, remaining)Try the right child only if the left found nothing.

8Dry run using the call stack

Each call is written as (node, target it received).

  1. (5, 22): not None, not a leaf → remaining 17 → call left. (5,22) waits.
  2. (4, 17): not a leaf (it has 11) → remaining 13 → call left. (4,17) waits.
  3. (11, 13): not a leaf → remaining 2 → call left.
  4. (7, 2): leaf! Is 2 == 7? No → False. This call leaves the stack.
  5. Back in (11, 13): left said False, so or calls the right: (2, 2): leaf! Is 2 == 2? Yes → True. It leaves the stack.
  6. (11, 13) gets False or True = True → returns to (4, 17) and leaves the stack.
  7. (4, 17): left gave True, so the right call (None) is skipped → returns True.
  8. (5, 22): left gave True → the whole right side (8, 13, 4, 1) is skipped → final answer True ✓
step 4 (deepest)
(5,22)(4,17)(11,13)(7,2) → False
step 5
(5,22)(4,17)(11,13)(2,2) → True
step 8
(5,22) → True

The teacher's point about space: (7, 2) left the stack before (2, 2) came in. At any moment the stack holds at most one call per level.

9Complexity & remember

Remember Path Sum (DFS) None → False · leaf → target == val · else ask left or right with target − val. Judge the sum only at a leaf.

Part B · Path Sum with BFS

Same question, same rules, but we walk the tree level by level with a queue.

1The question & constraints

Same as Part A. One constraint matters more here: 0 nodes is allowed. So BFS needs a check at the top. If the root is None we must not put it in the queue and then read .left from it.

2Intuition: what does the queue need to hold?

In DFS, each call carried two things: the node and the target for that node. The call stack kept them together for us. A queue has no calls, so we must keep the pair together ourselves: each queue entry is (node, target for this node).

The teacher notes that Java needs a small Pair class for this and C++ has pair. In Python we just put a tuple in the queue: (node, target).

3Building the logic

Doubt: in Path Sum II we will need "backtracking". Do we need anything like that here?
→ No. Each queue entry carries its own target, already worked out for that node. Nothing is shared between entries, so there's nothing to undo. As soon as we find a good leaf, we return True from inside the while loop, and the function ends.

4Approach steps

  1. If root is None → return False.
  2. Queue starts with (root, targetSum).
  3. While the queue isn't empty: pop (node, target).
  4. Leaf and target == node.val → return True.
  5. Push (node.left, target − node.val) if left exists, and the same for right.
  6. After the loop → return False.

5Code (Python)

Path Sum with BFS
from collections import deque

class Solution:
    def hasPathSum(self, root, targetSum):
        if root is None:                          # 0 nodes is allowed
            return False
        queue = deque([(root, targetSum)])        # (node, target for this node)
        while queue:
            node, target = queue.popleft()
            if node.left is None and node.right is None:
                if target == node.val:            # good leaf: done
                    return True
            remaining = target - node.val
            if node.left:
                queue.append((node.left, remaining))
            if node.right:
                queue.append((node.right, remaining))
        return False

6Code line by line

linewhat it means
if root is None: return FalseThe BFS base case. Needed only because the tree may be empty.
deque([(root, targetSum)])The first pair: the root needs the full target.
node, target = queue.popleft()Take out one pair. target is what this node and the nodes below it must add up to.
leaf and target == node.valThe same leaf test as DFS. If it passes, return True from inside the loop.
remaining = target - node.valWhat the children still need.
if node.left: append(...)Push only real children, each with its own remaining target.
return FalseEvery leaf was checked and none matched.

7Dry run: watch the queue

Same tree, target 22. Each box is (node, target). Yellow = popped now.

start5,22
step 1pop (5,22): not a leaf → push (4,17), (8,17)
4,178,17
step 2pop (4,17): not a leaf → push (11,13)
8,1711,13
step 3pop (8,17): not a leaf → push (13,9), (4,9)
11,1313,94,9
step 4pop (11,13): not a leaf → push (7,2), (2,2)
13,94,97,22,2
step 5pop (13,9): leaf, but 9 ≠ 13 → not valid, nothing to push
4,97,22,2
step 6pop (4,9): not a leaf (it has 1) → push (1,5)
7,22,21,5
step 7pop (7,2): leaf, but 2 ≠ 7 → not valid
2,21,5
step 8pop (2,2): leaf, 2 == 2 → return True ✓ ((1,5) is never checked)

Compare with DFS: DFS reached the answer after 5 calls on real nodes (5, 4, 11, 7, 2). BFS popped 8 pairs, including 8, 13 and the lower 4 from the right side. That's the "best case is better in DFS" point from Part A.

8Complexity & why BFS ran slower

On LeetCode, the BFS version ran a bit slower. The teacher's reason: for every node we build a new pair and do queue operations, which cost more than plain recursive calls. Her verdict: for path problems, go with DFS. It's simpler and usually faster.

Remember Path Sum (BFS)Queue of (node, target) pairs. Pop → leaf and target == val → True. Push real children with target − val. Empty queue → False. Needs a root-None check because n can be 0.

Part C · Revision page

DFS (recursion)BFS (queue)
carries the target inthe function argument (each call has its own)a tuple (node, target) in the queue
base casealways needed: None → Falseneeded only because n can be 0
leaf testtarget == node.valtarget == node.val
childrenleft or right, with target − valpush real children with target − val
best casestops after the first good pathmust go through upper levels first
time / spaceO(n) / O(h)O(n) / O(n)
teacher's pickyesworks, slower (pairs + queue ops)
constraint checkworst valueverdict
max sum, skewed tree5000 × 1000 = 5 × 10⁶fits in int
max sum, balanced tree≈ 13 × 1000 = 13,000fits in int
stepsn = 5000far below 10⁸
If you remember only 5 lines 1. Carry the target down. At each node, subtract its value for the children.
2. None → False, even when the target is 0 (no node means no path).
3. Decide only at a leaf: target == node.val.
4. Combine children with or: one good path is enough.
5. Prefer DFS for path problems. BFS works with (node, target) pairs.
Mistakes to avoid ✗ checking target == 0 at a leaf before subtracting the leaf's value
✗ returning True for an empty tree when target is 0
✗ judging the sum at a node that still has a child (not a leaf)
✗ treating a one-child node as a leaf (only one child missing ≠ leaf)
✗ sending the same target to the children instead of target − val
✗ BFS without the root-None check (crash on an empty tree)
test it yourself (paste under any of the solutions above)
root = TreeNode(5,
    TreeNode(4, TreeNode(11, TreeNode(7), TreeNode(2))),
    TreeNode(8, TreeNode(13), TreeNode(4, None, TreeNode(1))))

s = Solution()
print(s.hasPathSum(root, 22))                      # True  (5-4-11-2)
print(s.hasPathSum(root, 26))                      # True  (5-8-13)
print(s.hasPathSum(root, 9))                       # False (5-4 stops at 4, not a leaf)
print(s.hasPathSum(None, 0))                       # False (empty tree)
print(s.hasPathSum(TreeNode(1, TreeNode(2)), 1))   # False (1 is not a leaf)

Based on this video: Path Sum | DFS & BFS