Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Depth-First Search (DFS)

Builds on Top-Down DFS, which you haven’t finished yet.

P3MediumBuilt on Top-Down DFS~4 min speedrun

Root-to-Leaf Path Backtracking

Built on
Top-Down DFSThe path above is a shared list: append before the recursive calls, pop after them, and copy it at a leaf.
The problem

Path Sum II (LC 113)(opens leetcode.com)

You're given the root of a binary tree and a target number targetSum. Collect every root-to-leaf path whose node values add up to targetSum, with each path reported as a plain list of values (not node objects).

A root-to-leaf path always starts at the root and ends at a leaf — a node with no children at all.

Example 1
Input:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
Output:[[5,4,11,2],[5,8,4,5]]
There are two paths whose sum equals targetSum: 5 + 4 + 11 + 2 = 22 5 + 8 + 4 + 5 = 22
711245138541

Root-to-Leaf Paths

Choose ➔ Explore ➔ Un-choose
Spot it when: You must list or combine whole root-to-leaf paths: all paths with a target sum, path strings, numbers spelled along a path.
Root-to-Leaf Paths
Python
1from typing import Optional
2 
3class TreeNode:
4 def __init__(self, val: int = 0, left: Optional["TreeNode"] = None, right: Optional["TreeNode"] = None):
5 self.val = val
6 self.left = left
7 self.right = right
8 
9def path_sum(root: Optional[TreeNode], target: int) -> list[list[int]]:
10 """Path Sum II: every root-to-leaf path whose values add up to target.
11 Time: O(N * H) (copying each found path) | Space: O(H) path + recursion stack
12 """
13 results: list[list[int]] = []
14 path: list[int] = [] # One shared buffer, grown and shrunk symmetrically
15 
16 def dfs(node: Optional[TreeNode], rem: int) -> None:
17 if not node:
18 return
19 path.append(node.val) # 1. CHOOSE
20 if not node.left and not node.right and rem == node.val:
21 results.append(list(path)) # 📸 SNAPSHOT a copy at a valid leaf
22 dfs(node.left, rem - node.val) # 2. EXPLORE both children
23 dfs(node.right, rem - node.val)
24 path.pop() # 3. UN-CHOOSE (restore state on return)
25 
26 dfs(root, target)
27 return results
Invariant: path holds exactly the values from the root down to the current node, and rem is target minus the sum of the values above the current node.

One shared path list always holds exactly the nodes from the root to the current node, because every append on the way in has a matching pop on the way out. At a leaf whose value equals the remaining sum rem, save list(path), a copy, since the shared list keeps changing after you return. Pop after both children, even when the node matched; an early return before pop() leaves a stale value in every later path.

See it run

Root-to-leaf paths with one shared path list, step by step

node none · rem - · results []

node 5 · rem 22 · results []

node 4 · rem 17 · results []

node 11 · rem 13 · results []

node 7 · rem 2 · results []

node 7 · rem 2 · results []

node 2 · rem 2 · results []

node 2 · rem 2 · results [[5, 4, 11, 2]]

node 2 · rem 2 · results [[5, 4, 11, 2]]

node 11 · rem 13 · results [[5, 4, 11, 2]]

node 4 · rem 17 · results [[5, 4, 11, 2]]

node 8 · rem 17 · results [[5, 4, 11, 2]]

node 13 · rem 9 · results [[5, 4, 11, 2]]

node 13 · rem 9 · results [[5, 4, 11, 2]]

node 4 · rem 9 · results [[5, 4, 11, 2]]

node 5 · rem 5 · results [[5, 4, 11, 2]]

node 5 · rem 5 · results [[5, 4, 11, 2], [5, 8, 4, 5]]

node 5 · rem 5 · results [[5, 4, 11, 2], [5, 8, 4, 5]]

node 1 · rem 5 · results [[5, 4, 11, 2], [5, 8, 4, 5]]

node 1 · rem 5 · results [[5, 4, 11, 2], [5, 8, 4, 5]]

node 4 · rem 9 · results [[5, 4, 11, 2], [5, 8, 4, 5]]

node 8 · rem 17 · results [[5, 4, 11, 2], [5, 8, 4, 5]]

node 5 · rem 22 · results [[5, 4, 11, 2], [5, 8, 4, 5]]

node none · rem - · results [[5, 4, 11, 2], [5, 8, 4, 5]]

Each circle is a tree node. The path row shows the shared list, with its end on the right. The results row holds the copies.

  • node the node being entered or left
  • on path a node whose value is in the shared path list
  • copied the nodes whose values were copied into results

Call dfs on the root 5 with rem = 22. path is empty.

Enter 5 with rem = 22. Append 5 to path, so path is [5].

Enter 4 with rem = 17. Append 4 to path, so path is [5, 4].

Enter 11 with rem = 13. Append 11 to path, so path is [5, 4, 11].

Enter 7 with rem = 2. Append 7 to path, so path is [5, 4, 11, 7]. This is a leaf, but rem 2 is not 7, so nothing is saved.

7 is a leaf, so there is nothing more to explore. Pop 7, so path is [5, 4, 11].

Enter 2 with rem = 2. Append 2 to path, so path is [5, 4, 11, 2]. This is a leaf and rem equals 2.

Save list(path), a copy: [5, 4, 11, 2]. The shared path list will keep changing.

2 is a leaf, so there is nothing more to explore. Pop 2, so path is [5, 4, 11].

Both children of 11 are done. Pop 11, so path is [5, 4].

Both children of 4 are done. Pop 4, so path is [5].

Enter 8 with rem = 17. Append 8 to path, so path is [5, 8].

Enter 13 with rem = 9. Append 13 to path, so path is [5, 8, 13]. This is a leaf, but rem 9 is not 13, so nothing is saved.

13 is a leaf, so there is nothing more to explore. Pop 13, so path is [5, 8].

Enter 4 with rem = 9. Append 4 to path, so path is [5, 8, 4].

Enter 5 with rem = 5. Append 5 to path, so path is [5, 8, 4, 5]. This is a leaf and rem equals 5.

Save list(path), a copy: [5, 8, 4, 5]. The shared path list will keep changing.

5 is a leaf, so there is nothing more to explore. Pop 5, so path is [5, 8, 4].

Enter 1 with rem = 5. Append 1 to path, so path is [5, 8, 4, 1]. This is a leaf, but rem 5 is not 1, so nothing is saved.

1 is a leaf, so there is nothing more to explore. Pop 1, so path is [5, 8, 4].

Both children of 4 are done. Pop 4, so path is [5, 8].

Both children of 8 are done. Pop 8, so path is [5].

Both children of 5 are done. Pop 5, so path is [].

The root is popped and path is empty. results holds 2 paths: [[5, 4, 11, 2], [5, 8, 4, 5]].

711245138541
path
empty
results
empty
1 / 24
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Returning early before path.pop() runs

    Mistake
    if not node.left and not node.right and rem == node.val:
        results.append(list(path))
        return
    Fix
    if not node.left and not node.right and rem == node.val:
        results.append(list(path))

    What happens: Every path.append needs a matching path.pop(). An early return after a match skips that pop. The leaf's value then stays in path. Each later pop then removes the value one level too high. In the example, the second answer starts [5, 4, 8] instead of [5, 8].

    Example: Path Sum II (LC 113): root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
    Mistake: returns [[5, 4, 11, 2], [5, 4, 8, 4, 5]]Fix: returns [[5, 4, 11, 2], [5, 8, 4, 5]]
  2. 2

    Gotcha 2: Saving path instead of a copy, list(path)

    Mistake
    results.append(path)
    Fix
    results.append(list(path))

    What happens: results.append(path) stores the shared list itself, not its values. The walk keeps changing that list after the save. When the walk ends, every pop has run, so the list is empty. Every stored answer is then that same empty list.

    Example: root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
    Mistake: returns [[], []]Fix: returns [[5, 4, 11, 2], [5, 8, 4, 5]]
  3. 3

    Gotcha 3: Saving whenever the sum matches, not only at a leaf

    Mistake
    if rem == node.val:
    Fix
    if not node.left and not node.right and rem == node.val:

    What happens: A path in Path Sum II (LC 113) must end at a leaf, a node with no children. Without the leaf test, a node inside the tree whose sum matches is saved too. That path stops halfway down, so it is not an answer.

    Example: root = [1,2], targetSum = 1
    Mistake: returns [[1]]Fix: returns []
  4. 4

    Gotcha 4: Reading path from the root instead of the leaf

    Mistake
    s = "".join(path)
    Fix
    s = "".join(reversed(path))

    What happens: In Smallest String Starting From Leaf (LC 988), each string is read from the leaf up to the root. path holds the letters from the root down. So the string is path reversed. Comparing the root-first strings can pick a different leaf.

    Example: root = [0,1,2,3,4,3,4]
    Mistake: returns 'abd'Fix: returns 'dba'
Not this one if…
  • You only need a yes/no or a count, as in Path Sum (LC 112), not the list of nodes on each matching path. See Top-Down DFS
  • The choices come from a flat list and any group of picked items is an answer, as in Subsets (LC 78), not one path from the root to a leaf. See Subsets

Deep Dive

Why path always holds the route from the root to the current node

path is one list that all calls share. The blueprint changes it in a fixed pattern. A call to dfs(node, rem) does path.append(node.val) first. It then calls dfs on both children. It does path.pop() last, after both children return.

Each call adds one value and removes the same value before it returns. So when a call returns, path is the same as it was when the call started. While a call runs, path holds the values of the nodes from the root to that node, and nothing else. rem is target minus the sum of the values above the node, so at a leaf, rem == node.val means the whole path adds up to target.

Example: Path Sum II (LC 113) with root = [5, 4, 8, 11, null, 13, 4, 7, 2, null, null, 5, 1] and targetSum = 22. The walk makes 10 appends and 10 pops. The leaf 7 has rem = 2, so it is not saved. The leaf 2 has rem = 2, so [5, 4, 11, 2] is saved. The leaf 13 is not saved, the leaf 5 saves [5, 8, 4, 5], and the leaf 1 is not saved. The result is [[5, 4, 11, 2], [5, 8, 4, 5]].

The trap: the saved path must be a copy

results.append(path) stores the list itself, not its values. The walk keeps changing that list. At the end every pop has run, so path is empty. Every saved item is the same empty list. On the example above, the function returns [[], []] and not [[5, 4, 11, 2], [5, 8, 4, 5]].

results.append(list(path)) makes a new list with the current values. Later changes to path do not touch it. A copy of a path of length L costs O(L).

The trap: pop() must run after a match too

A match is a good result, so it is tempting to write return right after the save. The return skips path.pop(). The value of the leaf stays in path.

Every later pop then removes the value one level too high. On the example, the second answer is [5, 4, 8, 4, 5] and not [5, 8, 4, 5]. The 4 from the first branch was never removed.

The same two lines of the blueprint prevent both faults. Save with list(path), then let the code fall through to the two recursive calls and to path.pop(). Do not return early.

One more fault is to save on rem == node.val without the leaf test. For root = [1, 2] and targetSum = 1, the node 1 has rem == 1, but it is not a leaf. The correct result is []. The leaf test not node.left and not node.right is part of the match.

The cost, and the neighbour cards

Each node is entered once, with one append and one pop, so the walk is O(N). The extra cost is the copies. Each saved path costs O(H), where H is the height of the tree, so the total is O(N * H) in the worst case. The space is O(H) for path and for the recursion stack, not counting the saved results.

Example: the walk above saves 2 paths of 4 values each, so the copies cost 8 value moves in total.

Path Sum (LC 112) is the neighbour card. It only asks for yes or no, so a value passed down is enough and there is no path. Subsets (LC 78) also appends, recurses and pops, but its choices come from a flat list and every group of choices is an answer. Here the choices follow the edges of a tree and only a root-to-leaf path is an answer.

Binary Tree Paths (LC 257) saves every path as text. Smallest String Starting From Leaf (LC 988) reads path backward, from the leaf up to the root.

60-Second Pitch

"I need every root-to-leaf path with a given sum, so I use depth-first search with one shared list path. At each node I append its value, call both children with rem - node.val, and pop the value. Every append has a matching pop, so path always holds the route from the root to the current node. At a leaf where rem equals the leaf value, I save list(path), a copy, because the shared list keeps changing. I never return before the pop. The walk is O(N) and the copies make the worst case O(N * H). The space is O(H)."

Common Questions

Why do you save list(path) and not path?

path is one list that all calls share. results.append(path) stores that same list. By the end, every pop has run, so the list is empty, and every saved path shows as []. list(path) makes a new list with the values at that moment.

What is the time complexity of Path Sum II (LC 113)?

The walk is O(N), because each node is entered once. Each saved path is copied, and a copy costs up to O(H), where H is the height of the tree. So the worst case is O(N * H). The space is O(H) for path and the recursion, not counting the output.

Why must the pop run even after a match?

Each append needs one pop. If a match returns early, its pop is skipped and the leaf's value stays in path. Then every later pop removes a value one level too high. On the example the second answer would be [5, 4, 8, 4, 5].

How is it different from Path Sum (LC 112)?

Path Sum only needs yes or no. A number passed down, rem, is enough, and no path exists. Path Sum II needs the nodes of each matching path, so it also keeps path with an append before the calls and a pop after them, and it copies at a leaf.

Could you pass a new list down and avoid pop()?

Yes. Call dfs(node.left, rem - node.val, path + [node.val]). Each call has its own list, so there is nothing to undo and nothing to copy again. The cost is that every call builds a list of length up to H, so each node costs O(H). It is simpler and slower than one shared list.

SWE Reasoning

Spot it

  • The task asks for the paths themselves, not only for a count or a yes or no: "return all root-to-leaf paths".
  • Each path ends at a leaf, and a path is checked against a target such as a sum.
  • The wording says "root-to-leaf", "paths" or "every route".
  • The state of a path is a list that can grow by one value and shrink by one value.
  • The answer is a list of lists, so every saved path must be a separate copy.

The invariant, step by step

  1. path starts empty and dfs(root, target) is called.
  2. dfs(node, rem) starts with path.append(node.val). Now path holds the values from the root to node.
  3. rem is target minus the sum of the values above node. At a leaf, rem == node.val means the whole path adds up to target.
  4. At such a leaf, results.append(list(path)) saves a copy, because path will change again.
  5. The call passes rem - node.val to both children. Each child call appends and removes its own value, so path is the same when the child returns.
  6. path.pop() removes node.val. Now path holds the values from the root to the parent, which is what the parent expects.

Traps

  • results.append(path). It saves the shared list. At the end it is empty. Save list(path).
  • return right after a match. It skips path.pop(), and a stale value stays in every later path. Let the code reach the pop.
  • No leaf test. A node in the middle of the tree can match the sum, but its path does not end at a leaf. Test not node.left and not node.right.
  • Pop placed before the recursive calls. The children then see a path without this node.
  • Reading path in the wrong direction. path goes from the root down. If the answer is read from the leaf up, reverse it.

Practice

Canonical LeetCode:Path Sum II (LC 113)Binary Tree Paths (LC 257)Smallest String Starting From Leaf (LC 988)Step-By-Step Directions From a Binary Tree Node to Another (LC 2096)
Theory Context•Depth-First Search (DFS)

Builds on Top-Down DFS, which you haven’t finished yet.

P3MediumBuilt on Top-Down DFS~4 min speedrun

Root-to-Leaf Path Backtracking

Built on
Top-Down DFSThe path above is a shared list: append before the recursive calls, pop after them, and copy it at a leaf.
The problem

Path Sum II (LC 113)(opens leetcode.com)

You're given the root of a binary tree and a target number targetSum. Collect every root-to-leaf path whose node values add up to targetSum, with each path reported as a plain list of values (not node objects).

A root-to-leaf path always starts at the root and ends at a leaf — a node with no children at all.

Example 1
Input:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
Output:[[5,4,11,2],[5,8,4,5]]
There are two paths whose sum equals targetSum: 5 + 4 + 11 + 2 = 22 5 + 8 + 4 + 5 = 22
711245138541

Root-to-Leaf Paths

Choose ➔ Explore ➔ Un-choose
Spot it when: You must list or combine whole root-to-leaf paths: all paths with a target sum, path strings, numbers spelled along a path.
Root-to-Leaf Paths
Python
1from typing import Optional
2 
3class TreeNode:
4 def __init__(self, val: int = 0, left: Optional["TreeNode"] = None, right: Optional["TreeNode"] = None):
5 self.val = val
6 self.left = left
7 self.right = right
8 
9def path_sum(root: Optional[TreeNode], target: int) -> list[list[int]]:
10 """Path Sum II: every root-to-leaf path whose values add up to target.
11 Time: O(N * H) (copying each found path) | Space: O(H) path + recursion stack
12 """
13 results: list[list[int]] = []
14 path: list[int] = [] # One shared buffer, grown and shrunk symmetrically
15 
16 def dfs(node: Optional[TreeNode], rem: int) -> None:
17 if not node:
18 return
19 path.append(node.val) # 1. CHOOSE
20 if not node.left and not node.right and rem == node.val:
21 results.append(list(path)) # 📸 SNAPSHOT a copy at a valid leaf
22 dfs(node.left, rem - node.val) # 2. EXPLORE both children
23 dfs(node.right, rem - node.val)
24 path.pop() # 3. UN-CHOOSE (restore state on return)
25 
26 dfs(root, target)
27 return results
Invariant: path holds exactly the values from the root down to the current node, and rem is target minus the sum of the values above the current node.

One shared path list always holds exactly the nodes from the root to the current node, because every append on the way in has a matching pop on the way out. At a leaf whose value equals the remaining sum rem, save list(path), a copy, since the shared list keeps changing after you return. Pop after both children, even when the node matched; an early return before pop() leaves a stale value in every later path.

See it run

Root-to-leaf paths with one shared path list, step by step

node none · rem - · results []

node 5 · rem 22 · results []

node 4 · rem 17 · results []

node 11 · rem 13 · results []

node 7 · rem 2 · results []

node 7 · rem 2 · results []

node 2 · rem 2 · results []

node 2 · rem 2 · results [[5, 4, 11, 2]]

node 2 · rem 2 · results [[5, 4, 11, 2]]

node 11 · rem 13 · results [[5, 4, 11, 2]]

node 4 · rem 17 · results [[5, 4, 11, 2]]

node 8 · rem 17 · results [[5, 4, 11, 2]]

node 13 · rem 9 · results [[5, 4, 11, 2]]

node 13 · rem 9 · results [[5, 4, 11, 2]]

node 4 · rem 9 · results [[5, 4, 11, 2]]

node 5 · rem 5 · results [[5, 4, 11, 2]]

node 5 · rem 5 · results [[5, 4, 11, 2], [5, 8, 4, 5]]

node 5 · rem 5 · results [[5, 4, 11, 2], [5, 8, 4, 5]]

node 1 · rem 5 · results [[5, 4, 11, 2], [5, 8, 4, 5]]

node 1 · rem 5 · results [[5, 4, 11, 2], [5, 8, 4, 5]]

node 4 · rem 9 · results [[5, 4, 11, 2], [5, 8, 4, 5]]

node 8 · rem 17 · results [[5, 4, 11, 2], [5, 8, 4, 5]]

node 5 · rem 22 · results [[5, 4, 11, 2], [5, 8, 4, 5]]

node none · rem - · results [[5, 4, 11, 2], [5, 8, 4, 5]]

Each circle is a tree node. The path row shows the shared list, with its end on the right. The results row holds the copies.

  • node the node being entered or left
  • on path a node whose value is in the shared path list
  • copied the nodes whose values were copied into results

Call dfs on the root 5 with rem = 22. path is empty.

Enter 5 with rem = 22. Append 5 to path, so path is [5].

Enter 4 with rem = 17. Append 4 to path, so path is [5, 4].

Enter 11 with rem = 13. Append 11 to path, so path is [5, 4, 11].

Enter 7 with rem = 2. Append 7 to path, so path is [5, 4, 11, 7]. This is a leaf, but rem 2 is not 7, so nothing is saved.

7 is a leaf, so there is nothing more to explore. Pop 7, so path is [5, 4, 11].

Enter 2 with rem = 2. Append 2 to path, so path is [5, 4, 11, 2]. This is a leaf and rem equals 2.

Save list(path), a copy: [5, 4, 11, 2]. The shared path list will keep changing.

2 is a leaf, so there is nothing more to explore. Pop 2, so path is [5, 4, 11].

Both children of 11 are done. Pop 11, so path is [5, 4].

Both children of 4 are done. Pop 4, so path is [5].

Enter 8 with rem = 17. Append 8 to path, so path is [5, 8].

Enter 13 with rem = 9. Append 13 to path, so path is [5, 8, 13]. This is a leaf, but rem 9 is not 13, so nothing is saved.

13 is a leaf, so there is nothing more to explore. Pop 13, so path is [5, 8].

Enter 4 with rem = 9. Append 4 to path, so path is [5, 8, 4].

Enter 5 with rem = 5. Append 5 to path, so path is [5, 8, 4, 5]. This is a leaf and rem equals 5.

Save list(path), a copy: [5, 8, 4, 5]. The shared path list will keep changing.

5 is a leaf, so there is nothing more to explore. Pop 5, so path is [5, 8, 4].

Enter 1 with rem = 5. Append 1 to path, so path is [5, 8, 4, 1]. This is a leaf, but rem 5 is not 1, so nothing is saved.

1 is a leaf, so there is nothing more to explore. Pop 1, so path is [5, 8, 4].

Both children of 4 are done. Pop 4, so path is [5, 8].

Both children of 8 are done. Pop 8, so path is [5].

Both children of 5 are done. Pop 5, so path is [].

The root is popped and path is empty. results holds 2 paths: [[5, 4, 11, 2], [5, 8, 4, 5]].

711245138541
path
empty
results
empty
1 / 24
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Returning early before path.pop() runs

    Mistake
    if not node.left and not node.right and rem == node.val:
        results.append(list(path))
        return
    Fix
    if not node.left and not node.right and rem == node.val:
        results.append(list(path))

    What happens: Every path.append needs a matching path.pop(). An early return after a match skips that pop. The leaf's value then stays in path. Each later pop then removes the value one level too high. In the example, the second answer starts [5, 4, 8] instead of [5, 8].

    Example: Path Sum II (LC 113): root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
    Mistake: returns [[5, 4, 11, 2], [5, 4, 8, 4, 5]]Fix: returns [[5, 4, 11, 2], [5, 8, 4, 5]]
  2. 2

    Gotcha 2: Saving path instead of a copy, list(path)

    Mistake
    results.append(path)
    Fix
    results.append(list(path))

    What happens: results.append(path) stores the shared list itself, not its values. The walk keeps changing that list after the save. When the walk ends, every pop has run, so the list is empty. Every stored answer is then that same empty list.

    Example: root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
    Mistake: returns [[], []]Fix: returns [[5, 4, 11, 2], [5, 8, 4, 5]]
  3. 3

    Gotcha 3: Saving whenever the sum matches, not only at a leaf

    Mistake
    if rem == node.val:
    Fix
    if not node.left and not node.right and rem == node.val:

    What happens: A path in Path Sum II (LC 113) must end at a leaf, a node with no children. Without the leaf test, a node inside the tree whose sum matches is saved too. That path stops halfway down, so it is not an answer.

    Example: root = [1,2], targetSum = 1
    Mistake: returns [[1]]Fix: returns []
  4. 4

    Gotcha 4: Reading path from the root instead of the leaf

    Mistake
    s = "".join(path)
    Fix
    s = "".join(reversed(path))

    What happens: In Smallest String Starting From Leaf (LC 988), each string is read from the leaf up to the root. path holds the letters from the root down. So the string is path reversed. Comparing the root-first strings can pick a different leaf.

    Example: root = [0,1,2,3,4,3,4]
    Mistake: returns 'abd'Fix: returns 'dba'
Not this one if…
  • You only need a yes/no or a count, as in Path Sum (LC 112), not the list of nodes on each matching path. See Top-Down DFS
  • The choices come from a flat list and any group of picked items is an answer, as in Subsets (LC 78), not one path from the root to a leaf. See Subsets

Deep Dive

Why path always holds the route from the root to the current node

path is one list that all calls share. The blueprint changes it in a fixed pattern. A call to dfs(node, rem) does path.append(node.val) first. It then calls dfs on both children. It does path.pop() last, after both children return.

Each call adds one value and removes the same value before it returns. So when a call returns, path is the same as it was when the call started. While a call runs, path holds the values of the nodes from the root to that node, and nothing else. rem is target minus the sum of the values above the node, so at a leaf, rem == node.val means the whole path adds up to target.

Example: Path Sum II (LC 113) with root = [5, 4, 8, 11, null, 13, 4, 7, 2, null, null, 5, 1] and targetSum = 22. The walk makes 10 appends and 10 pops. The leaf 7 has rem = 2, so it is not saved. The leaf 2 has rem = 2, so [5, 4, 11, 2] is saved. The leaf 13 is not saved, the leaf 5 saves [5, 8, 4, 5], and the leaf 1 is not saved. The result is [[5, 4, 11, 2], [5, 8, 4, 5]].

The trap: the saved path must be a copy

results.append(path) stores the list itself, not its values. The walk keeps changing that list. At the end every pop has run, so path is empty. Every saved item is the same empty list. On the example above, the function returns [[], []] and not [[5, 4, 11, 2], [5, 8, 4, 5]].

results.append(list(path)) makes a new list with the current values. Later changes to path do not touch it. A copy of a path of length L costs O(L).

The trap: pop() must run after a match too

A match is a good result, so it is tempting to write return right after the save. The return skips path.pop(). The value of the leaf stays in path.

Every later pop then removes the value one level too high. On the example, the second answer is [5, 4, 8, 4, 5] and not [5, 8, 4, 5]. The 4 from the first branch was never removed.

The same two lines of the blueprint prevent both faults. Save with list(path), then let the code fall through to the two recursive calls and to path.pop(). Do not return early.

One more fault is to save on rem == node.val without the leaf test. For root = [1, 2] and targetSum = 1, the node 1 has rem == 1, but it is not a leaf. The correct result is []. The leaf test not node.left and not node.right is part of the match.

The cost, and the neighbour cards

Each node is entered once, with one append and one pop, so the walk is O(N). The extra cost is the copies. Each saved path costs O(H), where H is the height of the tree, so the total is O(N * H) in the worst case. The space is O(H) for path and for the recursion stack, not counting the saved results.

Example: the walk above saves 2 paths of 4 values each, so the copies cost 8 value moves in total.

Path Sum (LC 112) is the neighbour card. It only asks for yes or no, so a value passed down is enough and there is no path. Subsets (LC 78) also appends, recurses and pops, but its choices come from a flat list and every group of choices is an answer. Here the choices follow the edges of a tree and only a root-to-leaf path is an answer.

Binary Tree Paths (LC 257) saves every path as text. Smallest String Starting From Leaf (LC 988) reads path backward, from the leaf up to the root.

60-Second Pitch

"I need every root-to-leaf path with a given sum, so I use depth-first search with one shared list path. At each node I append its value, call both children with rem - node.val, and pop the value. Every append has a matching pop, so path always holds the route from the root to the current node. At a leaf where rem equals the leaf value, I save list(path), a copy, because the shared list keeps changing. I never return before the pop. The walk is O(N) and the copies make the worst case O(N * H). The space is O(H)."

Common Questions

Why do you save list(path) and not path?

path is one list that all calls share. results.append(path) stores that same list. By the end, every pop has run, so the list is empty, and every saved path shows as []. list(path) makes a new list with the values at that moment.

What is the time complexity of Path Sum II (LC 113)?

The walk is O(N), because each node is entered once. Each saved path is copied, and a copy costs up to O(H), where H is the height of the tree. So the worst case is O(N * H). The space is O(H) for path and the recursion, not counting the output.

Why must the pop run even after a match?

Each append needs one pop. If a match returns early, its pop is skipped and the leaf's value stays in path. Then every later pop removes a value one level too high. On the example the second answer would be [5, 4, 8, 4, 5].

How is it different from Path Sum (LC 112)?

Path Sum only needs yes or no. A number passed down, rem, is enough, and no path exists. Path Sum II needs the nodes of each matching path, so it also keeps path with an append before the calls and a pop after them, and it copies at a leaf.

Could you pass a new list down and avoid pop()?

Yes. Call dfs(node.left, rem - node.val, path + [node.val]). Each call has its own list, so there is nothing to undo and nothing to copy again. The cost is that every call builds a list of length up to H, so each node costs O(H). It is simpler and slower than one shared list.

SWE Reasoning

Spot it

  • The task asks for the paths themselves, not only for a count or a yes or no: "return all root-to-leaf paths".
  • Each path ends at a leaf, and a path is checked against a target such as a sum.
  • The wording says "root-to-leaf", "paths" or "every route".
  • The state of a path is a list that can grow by one value and shrink by one value.
  • The answer is a list of lists, so every saved path must be a separate copy.

The invariant, step by step

  1. path starts empty and dfs(root, target) is called.
  2. dfs(node, rem) starts with path.append(node.val). Now path holds the values from the root to node.
  3. rem is target minus the sum of the values above node. At a leaf, rem == node.val means the whole path adds up to target.
  4. At such a leaf, results.append(list(path)) saves a copy, because path will change again.
  5. The call passes rem - node.val to both children. Each child call appends and removes its own value, so path is the same when the child returns.
  6. path.pop() removes node.val. Now path holds the values from the root to the parent, which is what the parent expects.

Traps

  • results.append(path). It saves the shared list. At the end it is empty. Save list(path).
  • return right after a match. It skips path.pop(), and a stale value stays in every later path. Let the code reach the pop.
  • No leaf test. A node in the middle of the tree can match the sum, but its path does not end at a leaf. Test not node.left and not node.right.
  • Pop placed before the recursive calls. The children then see a path without this node.
  • Reading path in the wrong direction. path goes from the root down. If the answer is read from the leaf up, reverse it.

Practice

Canonical LeetCode:Path Sum II (LC 113)Binary Tree Paths (LC 257)Smallest String Starting From Leaf (LC 988)Step-By-Step Directions From a Binary Tree Node to Another (LC 2096)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•