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•Graph Algorithms

Builds on Flood Fill, which you haven’t finished yet.

P3MediumBuilt on Flood Fill~5 min speedrun

Traverse an Adjacency List and Carry a Result (All Paths From Source to Target)

Built on
Flood FillThe graph is an adjacency list, and the walk carries a path that it extends, copies at the target and restores. No visited set: every path counts.
The problem

All Paths From Source to Target (LC 797)(opens leetcode.com)

A directed graph has n nodes, numbered 0 to n - 1, and it has no cycle. A directed graph with no cycle is called a DAG. An edge goes from one node to another and can be followed in that direction only.

The graph is given as graph, an adjacency list: graph[i] lists every node that node i has an edge to. A path is a list of nodes in which each node has an edge to the next one.

Find every path that starts at node 0 and ends at node n - 1. Return all of them, in any order.

Example 1
Input:graph = [[1,2],[3],[3],[]]
Output:[[0,1,3],[0,2,3]]
Node 0 leads to nodes 1 and 2, and both lead to node 3. There are two paths: 0, 1, 3 and 0, 2, 3.
0123

Graph Paths

adjacency list ➔ walk with one shared path ➔ copy at the target ➔ undo
Spot it when: You must list every path between two nodes of a graph that has no cycle and is given as an adjacency list, or carry a value from the start node down every path.
Graph Paths
Python
1from typing import List
2 
3def all_paths(graph: List[List[int]], source: int, target: int) -> List[List[int]]:
4 """Every path from source to target in a directed graph that has no cycle, given as
5 an adjacency list.
6 Precondition: the graph has no cycle, so every walk ends. Finding a cycle is the
7 Cycle Detection card's job.
8 Time: O(2^V * V) | Space: O(V)
9 """
10 paths: List[List[int]] = [] # Every finished path, each one a copy
11 path = [source] # The nodes from source to the node being visited
12 
13 def dfs(node: int) -> None:
14 # 1. Save A Copy At The Target:
15 # path[:] is a snapshot, because path keeps changing after this call returns.
16 if node == target:
17 paths.append(path[:])
18 return
19 
20 # 2. Extend The Path, Walk, Then Undo:
21 # No visited set: a node on two paths must be walked once for each path.
22 for nxt in graph[node]:
23 path.append(nxt)
24 dfs(nxt)
25 path.pop()
26 
27 dfs(source)
28 return paths
Invariant: When dfs(node) is called, path holds the nodes from source to node, and when dfs(node) returns, path is the same as it was before the call.

Every path from source to target is a separate answer, so a node that lies on two paths must be walked once for each path, and the walk keeps no visited set. One shared list path holds the nodes from source to the current node: path.append(nxt) before dfs(nxt), and path.pop() after it. When the walk reaches target, save path[:], a copy, because path keeps changing after the call returns. The graph has no cycle, so every walk ends. The trap is the visited check from Flood Fill: if nxt in seen: continue skips the second path to a shared node, so [[1, 2], [3], [3], []] returns one path instead of two.

See it run

All Paths From Source to Target, step by step

paths saved 0

paths saved 0

paths saved 0

paths saved 1

paths saved 1

paths saved 1

paths saved 1

paths saved 1

paths saved 2

paths saved 2

paths saved 2

paths saved 2

Each circle is a node and each arrow is an edge. The row path is the route from node 0 to the node being visited. The row paths holds the saved copies.

  • now the node the walk is at
  • on path a node in the shared list path
  • saved a node of a path that was copied into paths
  • 2nd path a node that the walk reaches a second time, by a different path

The graph has 4 nodes. The target is node 3. path starts as [0], and paths is empty.

path.append(1) gives [0, 1], and dfs(1) starts.

path.append(3) gives [0, 1, 3], and dfs(3) starts.

Node 3 is the target. paths.append(path[:]) saves the copy [0, 1, 3].

path.pop() removes 3, so path is [0, 1] again.

path.pop() removes 1, so path is [0] again.

path.append(2) gives [0, 2], and dfs(2) starts.

path.append(3) gives [0, 2, 3]. Node 3 was walked before, on another path. A seen set would skip it here.

Node 3 is the target. paths.append(path[:]) saves the copy [0, 2, 3].

path.pop() removes 3, so path is [0, 2] again.

path.pop() removes 2, so path is [0] again.

dfs(0) ends. The function returns 2 paths.

0now123
path
0
paths (saved copies)
empty
1 / 12
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Skipping a node that was already visited, as Flood Fill does

    Mistake
    for nxt in graph[node]:
        if nxt in seen:
            continue
        seen.add(nxt)
        path.append(nxt)
    Fix
    for nxt in graph[node]:
        path.append(nxt)

    What happens: Flood Fill marks a cell once, because it needs each cell once. This card needs each path once, and a node can lie on several paths. In the example, node 3 is reached first by the path [0, 1, 3]. Node 3 is then in seen, so if nxt in seen: continue skips it on the path through node 2. The path [0, 2, 3] is never found.

    Example: All Paths From Source to Target (LC 797): graph = [[1, 2], [3], [3], []], source = 0, target = 3
    Mistake: returns [[0, 1, 3]]Fix: returns [[0, 1, 3], [0, 2, 3]]
  2. 2

    Gotcha 2: Saving path instead of a copy, path[:]

    Mistake
    paths.append(path)
    Fix
    paths.append(path[:])

    What happens: paths.append(path) stores the shared list itself, not its values. The walk keeps changing that list after the save. When the walk ends, every path.pop() has run, and path is back to [source]. Every stored answer is then that same list.

    Example: graph = [[1, 2], [3], [3], []], source = 0, target = 3
    Mistake: returns [[0], [0]]Fix: returns [[0, 1, 3], [0, 2, 3]]
  3. 3

    Gotcha 3: Leaving out path.pop() after the call

    Mistake
    path.append(nxt)
    dfs(nxt)
    Fix
    path.append(nxt)
    dfs(nxt)
    path.pop()

    What happens: Every path.append needs a matching path.pop(). Without the pop, the nodes of the first path stay in path. The next neighbour is then added after them. In the example, the second answer starts with [0, 1, 3] and goes on to node 2 and node 3, so it is not a path of the graph.

    Example: graph = [[1, 2], [3], [3], []], source = 0, target = 3
    Mistake: returns [[0, 1, 3], [0, 1, 3, 2, 3]]Fix: returns [[0, 1, 3], [0, 2, 3]]
  4. 4

    Gotcha 4: Running the walk on a graph that has a cycle

    Mistake
    for nxt in graph[node]:
        path.append(nxt)
    Fix
    for nxt in graph[node]:
        if nxt in path:
            continue
        path.append(nxt)

    What happens: The blueprint has no visited set, so nothing stops a walk that goes around a cycle. In the example, node 0 leads to node 1, and node 1 leads back to node 0. The walk goes 0, 1, 0, 1 and so on, and the recursion never ends. Python raises a RecursionError. LeetCode promises a graph with no cycle. For any other graph, skip a node that is already in path. That skips only a node on the current path, so a node on two different paths is still walked twice.

    Example: graph = [[1], [0, 2], []], source = 0, target = 2 (node 1 leads back to node 0)
    Mistake: raises RecursionErrorFix: returns [[0, 1, 2]]
Not this one if…
  • You only need to know which nodes the walk can reach, or how many groups there are, as in Keys and Rooms (LC 841). A visited set then makes the walk touch each node once, and no path list is needed. See Flood Fill
  • The directed graph can have a cycle, and you must find it, as in Course Schedule (LC 207). This card assumes that no cycle exists. See Cycle Detection
  • The input is a binary tree and a path must end at a leaf, as in Path Sum II (LC 113). A tree needs no adjacency list, and each node has only a left and a right child. See Root-to-Leaf Paths

Deep Dive

Why path holds exactly the route to the current node

path starts as [0]. Before each call dfs(nxt), the code runs path.append(nxt). After the call returns, the code runs path.pop().

So when dfs(node) is called, path holds the nodes from node 0 to node. When dfs(node) returns, path is the same as before the call, because every append inside it has a matching pop.

Example: graph = [[1, 2], [3], [3], []]. The walk appends 1 and 3, saves the path, and pops 3 and 1. Then path is [0] again, and the neighbour 2 extends the same path.

Why a copy is saved, and why there is no visited set

paths.append(path[:]) stores the values of path at that moment. If the code stored path itself, every saved answer would be the same list. At the end that list is [0], so Example 1 would return [[0], [0]].

Flood Fill marks a cell as seen because it needs each cell once. This card needs each path once, and a node can lie on several paths. Node 3 lies on [0, 1, 3] and on [0, 2, 3].

With if nxt in seen: continue, the second path to node 3 is skipped, and Example 1 returns [[0, 1, 3]]. The graph has no cycle, so without a visited set every walk still ends.

One trace: node 3 is reached twice

The board runs allPathsSourceTarget on LeetCode's Example 1: graph = [[1, 2], [3], [3], []].

The walk appends 1, then 3. Node 3 is the target, so the copy [0, 1, 3] is saved. Two pops return path to [0].

The walk appends 2, then 3 again. This is the step marked with the trap tag: a visited set would skip it. Instead the copy [0, 2, 3] is saved.

The final answer is [[0, 1, 3], [0, 2, 3]], the same as LeetCode's output.

The cost, and carrying a value instead of a path

In the worst case every node has an edge to every later node. Then there are 2^(V - 2) paths, and each copy costs up to V. The time is O(2^V * V), and the extra space is O(V) for path and the recursion.

The same walk carries other results. Time Needed to Inform All Employees (LC 1376) carries the minutes so far, and takes the largest value. Employee Importance (LC 690) carries a sum. Clone Graph (LC 133) carries a map from each node to its copy, and that map must also act as a visited set, because that graph can have cycles.

60-Second Pitch

"I use a depth-first search with one shared path. The path starts with node 0. For each neighbour I append it to the path, call dfs, and pop it when the call returns, so the path always holds the route to the current node. At the last node I save a copy of the path, because the path keeps changing. I keep no visited set, because a node can lie on many paths and each path is a separate answer. The graph has no cycle, so every walk ends. The worst case has 2 to the power of n minus 2 paths, so the time is O(2^n * n) and the extra space is O(n)."

Common Questions

Why is there no visited set, when Flood Fill needs one?

Flood Fill needs each cell once. This card needs each path once, and one node can lie on many paths. A visited set would skip the second path to a shared node. A graph with no cycle also needs no visited set to stop the walk.

What happens if the graph has a cycle?

The walk goes around the cycle forever, and Python raises a RecursionError. Skip a node that is already in path, or use the Cycle Detection card first.

Why path[:] and not path?

path is one list that keeps changing. Storing it stores the same list many times. By the end every pop has run, so all saved answers would equal [0].

How many paths can there be?

At most 2^(n - 2), when every node has an edge to every later node. With n up to 15 this is 8192 paths, so listing them all is affordable.

SWE Reasoning

Spot it

  • The statement asks for all possible paths between two nodes, not a count and not a yes or no.
  • The graph is directed, has no cycle, and is given as an adjacency list.
  • The graph is small, because the answer can have 2^(n - 2) paths.
  • If the statement asks only whether a node is reachable, a visited set is enough and this is a different card.

The invariant, step by step

  1. path = [0], paths = [], and target = len(graph) - 1.
  2. dfs(node) first checks node == target. At the target it saves path[:] and returns.
  3. Otherwise, for each nxt in graph[node]: path.append(nxt), dfs(nxt), path.pop().
  4. When dfs(node) returns, path is the same as before the call.
  5. dfs(0) ends, and paths holds every path.

Traps

  • A visited set. if nxt in seen: continue loses the second path to a shared node.
  • Saving path without a copy. Every answer becomes the same list.
  • A missing path.pop(). The second answer starts with the nodes of the first.
  • A graph with a cycle. The walk never ends and Python raises a RecursionError.

Practice

Canonical LeetCode:All Paths From Source to Target (LC 797)Time Needed to Inform All Employees (LC 1376)Employee Importance (LC 690)
Theory Context•Graph Algorithms

Builds on Flood Fill, which you haven’t finished yet.

P3MediumBuilt on Flood Fill~5 min speedrun

Traverse an Adjacency List and Carry a Result (All Paths From Source to Target)

Built on
Flood FillThe graph is an adjacency list, and the walk carries a path that it extends, copies at the target and restores. No visited set: every path counts.
The problem

All Paths From Source to Target (LC 797)(opens leetcode.com)

A directed graph has n nodes, numbered 0 to n - 1, and it has no cycle. A directed graph with no cycle is called a DAG. An edge goes from one node to another and can be followed in that direction only.

The graph is given as graph, an adjacency list: graph[i] lists every node that node i has an edge to. A path is a list of nodes in which each node has an edge to the next one.

Find every path that starts at node 0 and ends at node n - 1. Return all of them, in any order.

Example 1
Input:graph = [[1,2],[3],[3],[]]
Output:[[0,1,3],[0,2,3]]
Node 0 leads to nodes 1 and 2, and both lead to node 3. There are two paths: 0, 1, 3 and 0, 2, 3.
0123

Graph Paths

adjacency list ➔ walk with one shared path ➔ copy at the target ➔ undo
Spot it when: You must list every path between two nodes of a graph that has no cycle and is given as an adjacency list, or carry a value from the start node down every path.
Graph Paths
Python
1from typing import List
2 
3def all_paths(graph: List[List[int]], source: int, target: int) -> List[List[int]]:
4 """Every path from source to target in a directed graph that has no cycle, given as
5 an adjacency list.
6 Precondition: the graph has no cycle, so every walk ends. Finding a cycle is the
7 Cycle Detection card's job.
8 Time: O(2^V * V) | Space: O(V)
9 """
10 paths: List[List[int]] = [] # Every finished path, each one a copy
11 path = [source] # The nodes from source to the node being visited
12 
13 def dfs(node: int) -> None:
14 # 1. Save A Copy At The Target:
15 # path[:] is a snapshot, because path keeps changing after this call returns.
16 if node == target:
17 paths.append(path[:])
18 return
19 
20 # 2. Extend The Path, Walk, Then Undo:
21 # No visited set: a node on two paths must be walked once for each path.
22 for nxt in graph[node]:
23 path.append(nxt)
24 dfs(nxt)
25 path.pop()
26 
27 dfs(source)
28 return paths
Invariant: When dfs(node) is called, path holds the nodes from source to node, and when dfs(node) returns, path is the same as it was before the call.

Every path from source to target is a separate answer, so a node that lies on two paths must be walked once for each path, and the walk keeps no visited set. One shared list path holds the nodes from source to the current node: path.append(nxt) before dfs(nxt), and path.pop() after it. When the walk reaches target, save path[:], a copy, because path keeps changing after the call returns. The graph has no cycle, so every walk ends. The trap is the visited check from Flood Fill: if nxt in seen: continue skips the second path to a shared node, so [[1, 2], [3], [3], []] returns one path instead of two.

See it run

All Paths From Source to Target, step by step

paths saved 0

paths saved 0

paths saved 0

paths saved 1

paths saved 1

paths saved 1

paths saved 1

paths saved 1

paths saved 2

paths saved 2

paths saved 2

paths saved 2

Each circle is a node and each arrow is an edge. The row path is the route from node 0 to the node being visited. The row paths holds the saved copies.

  • now the node the walk is at
  • on path a node in the shared list path
  • saved a node of a path that was copied into paths
  • 2nd path a node that the walk reaches a second time, by a different path

The graph has 4 nodes. The target is node 3. path starts as [0], and paths is empty.

path.append(1) gives [0, 1], and dfs(1) starts.

path.append(3) gives [0, 1, 3], and dfs(3) starts.

Node 3 is the target. paths.append(path[:]) saves the copy [0, 1, 3].

path.pop() removes 3, so path is [0, 1] again.

path.pop() removes 1, so path is [0] again.

path.append(2) gives [0, 2], and dfs(2) starts.

path.append(3) gives [0, 2, 3]. Node 3 was walked before, on another path. A seen set would skip it here.

Node 3 is the target. paths.append(path[:]) saves the copy [0, 2, 3].

path.pop() removes 3, so path is [0, 2] again.

path.pop() removes 2, so path is [0] again.

dfs(0) ends. The function returns 2 paths.

0now123
path
0
paths (saved copies)
empty
1 / 12
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Skipping a node that was already visited, as Flood Fill does

    Mistake
    for nxt in graph[node]:
        if nxt in seen:
            continue
        seen.add(nxt)
        path.append(nxt)
    Fix
    for nxt in graph[node]:
        path.append(nxt)

    What happens: Flood Fill marks a cell once, because it needs each cell once. This card needs each path once, and a node can lie on several paths. In the example, node 3 is reached first by the path [0, 1, 3]. Node 3 is then in seen, so if nxt in seen: continue skips it on the path through node 2. The path [0, 2, 3] is never found.

    Example: All Paths From Source to Target (LC 797): graph = [[1, 2], [3], [3], []], source = 0, target = 3
    Mistake: returns [[0, 1, 3]]Fix: returns [[0, 1, 3], [0, 2, 3]]
  2. 2

    Gotcha 2: Saving path instead of a copy, path[:]

    Mistake
    paths.append(path)
    Fix
    paths.append(path[:])

    What happens: paths.append(path) stores the shared list itself, not its values. The walk keeps changing that list after the save. When the walk ends, every path.pop() has run, and path is back to [source]. Every stored answer is then that same list.

    Example: graph = [[1, 2], [3], [3], []], source = 0, target = 3
    Mistake: returns [[0], [0]]Fix: returns [[0, 1, 3], [0, 2, 3]]
  3. 3

    Gotcha 3: Leaving out path.pop() after the call

    Mistake
    path.append(nxt)
    dfs(nxt)
    Fix
    path.append(nxt)
    dfs(nxt)
    path.pop()

    What happens: Every path.append needs a matching path.pop(). Without the pop, the nodes of the first path stay in path. The next neighbour is then added after them. In the example, the second answer starts with [0, 1, 3] and goes on to node 2 and node 3, so it is not a path of the graph.

    Example: graph = [[1, 2], [3], [3], []], source = 0, target = 3
    Mistake: returns [[0, 1, 3], [0, 1, 3, 2, 3]]Fix: returns [[0, 1, 3], [0, 2, 3]]
  4. 4

    Gotcha 4: Running the walk on a graph that has a cycle

    Mistake
    for nxt in graph[node]:
        path.append(nxt)
    Fix
    for nxt in graph[node]:
        if nxt in path:
            continue
        path.append(nxt)

    What happens: The blueprint has no visited set, so nothing stops a walk that goes around a cycle. In the example, node 0 leads to node 1, and node 1 leads back to node 0. The walk goes 0, 1, 0, 1 and so on, and the recursion never ends. Python raises a RecursionError. LeetCode promises a graph with no cycle. For any other graph, skip a node that is already in path. That skips only a node on the current path, so a node on two different paths is still walked twice.

    Example: graph = [[1], [0, 2], []], source = 0, target = 2 (node 1 leads back to node 0)
    Mistake: raises RecursionErrorFix: returns [[0, 1, 2]]
Not this one if…
  • You only need to know which nodes the walk can reach, or how many groups there are, as in Keys and Rooms (LC 841). A visited set then makes the walk touch each node once, and no path list is needed. See Flood Fill
  • The directed graph can have a cycle, and you must find it, as in Course Schedule (LC 207). This card assumes that no cycle exists. See Cycle Detection
  • The input is a binary tree and a path must end at a leaf, as in Path Sum II (LC 113). A tree needs no adjacency list, and each node has only a left and a right child. See Root-to-Leaf Paths

Deep Dive

Why path holds exactly the route to the current node

path starts as [0]. Before each call dfs(nxt), the code runs path.append(nxt). After the call returns, the code runs path.pop().

So when dfs(node) is called, path holds the nodes from node 0 to node. When dfs(node) returns, path is the same as before the call, because every append inside it has a matching pop.

Example: graph = [[1, 2], [3], [3], []]. The walk appends 1 and 3, saves the path, and pops 3 and 1. Then path is [0] again, and the neighbour 2 extends the same path.

Why a copy is saved, and why there is no visited set

paths.append(path[:]) stores the values of path at that moment. If the code stored path itself, every saved answer would be the same list. At the end that list is [0], so Example 1 would return [[0], [0]].

Flood Fill marks a cell as seen because it needs each cell once. This card needs each path once, and a node can lie on several paths. Node 3 lies on [0, 1, 3] and on [0, 2, 3].

With if nxt in seen: continue, the second path to node 3 is skipped, and Example 1 returns [[0, 1, 3]]. The graph has no cycle, so without a visited set every walk still ends.

One trace: node 3 is reached twice

The board runs allPathsSourceTarget on LeetCode's Example 1: graph = [[1, 2], [3], [3], []].

The walk appends 1, then 3. Node 3 is the target, so the copy [0, 1, 3] is saved. Two pops return path to [0].

The walk appends 2, then 3 again. This is the step marked with the trap tag: a visited set would skip it. Instead the copy [0, 2, 3] is saved.

The final answer is [[0, 1, 3], [0, 2, 3]], the same as LeetCode's output.

The cost, and carrying a value instead of a path

In the worst case every node has an edge to every later node. Then there are 2^(V - 2) paths, and each copy costs up to V. The time is O(2^V * V), and the extra space is O(V) for path and the recursion.

The same walk carries other results. Time Needed to Inform All Employees (LC 1376) carries the minutes so far, and takes the largest value. Employee Importance (LC 690) carries a sum. Clone Graph (LC 133) carries a map from each node to its copy, and that map must also act as a visited set, because that graph can have cycles.

60-Second Pitch

"I use a depth-first search with one shared path. The path starts with node 0. For each neighbour I append it to the path, call dfs, and pop it when the call returns, so the path always holds the route to the current node. At the last node I save a copy of the path, because the path keeps changing. I keep no visited set, because a node can lie on many paths and each path is a separate answer. The graph has no cycle, so every walk ends. The worst case has 2 to the power of n minus 2 paths, so the time is O(2^n * n) and the extra space is O(n)."

Common Questions

Why is there no visited set, when Flood Fill needs one?

Flood Fill needs each cell once. This card needs each path once, and one node can lie on many paths. A visited set would skip the second path to a shared node. A graph with no cycle also needs no visited set to stop the walk.

What happens if the graph has a cycle?

The walk goes around the cycle forever, and Python raises a RecursionError. Skip a node that is already in path, or use the Cycle Detection card first.

Why path[:] and not path?

path is one list that keeps changing. Storing it stores the same list many times. By the end every pop has run, so all saved answers would equal [0].

How many paths can there be?

At most 2^(n - 2), when every node has an edge to every later node. With n up to 15 this is 8192 paths, so listing them all is affordable.

SWE Reasoning

Spot it

  • The statement asks for all possible paths between two nodes, not a count and not a yes or no.
  • The graph is directed, has no cycle, and is given as an adjacency list.
  • The graph is small, because the answer can have 2^(n - 2) paths.
  • If the statement asks only whether a node is reachable, a visited set is enough and this is a different card.

The invariant, step by step

  1. path = [0], paths = [], and target = len(graph) - 1.
  2. dfs(node) first checks node == target. At the target it saves path[:] and returns.
  3. Otherwise, for each nxt in graph[node]: path.append(nxt), dfs(nxt), path.pop().
  4. When dfs(node) returns, path is the same as before the call.
  5. dfs(0) ends, and paths holds every path.

Traps

  • A visited set. if nxt in seen: continue loses the second path to a shared node.
  • Saving path without a copy. Every answer becomes the same list.
  • A missing path.pop(). The second answer starts with the nodes of the first.
  • A graph with a cycle. The walk never ends and Python raises a RecursionError.

Practice

Canonical LeetCode:All Paths From Source to Target (LC 797)Time Needed to Inform All Employees (LC 1376)Employee Importance (LC 690)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•