Problem
Given a directed acyclic graph (DAG) with n nodes numbered 0 through nโ1, find every possible path from node 0 to node nโ1. The graph is given as an adjacency list where graph[i] lists the nodes that node i has an outgoing edge to.
- Input:
graph = [[1,2],[3],[3],[]] - Output:
[[0,1,3],[0,2,3]] - Explanation: From node 0 you can branch to node 1 or 2; both routes reach the destination node 3.
Intuition
Because the graph is a DAG (no cycles), every path from node 0 must eventually terminate โ it can never loop back to a node it already visited. This means we can explore every branch freely with depth-first search, recording a snapshot of the path each time we land on node nโ1, and simply pop the last node to backtrack when a branch is exhausted.
Solution โ DFS with Backtracking
Extend the current path one node at a time; when the path reaches node nโ1, record a copy of it. Because the graph has no cycles, no visited set is needed.
- Initialize
results = []and seedcurrent_path = [0]with the source node. - Call
dfs(0)to start exploration from node 0. - In
dfs(node): ifnode == n โ 1, append a copy ofcurrent_pathtoresultsand return. - For each
neighboringraph[node], appendneighbortocurrent_path, recurse intodfs(neighbor), then popneighborto backtrack. - Return
resultsafter the initial call completes.
1def allPathsSourceTarget(graph: list[list[int]]) -> list[list[int]]:
2 results = []
3 current_path = [0] # seed with source so it appears in every recorded path
4
5 def dfs(node: int) -> None:
6 if node == len(graph) - 1: # reached the destination
7 results.append(list(current_path)) # snapshot โ not a reference
8 return
9 for neighbor in graph[node]:
10 current_path.append(neighbor)
11 dfs(neighbor)
12 current_path.pop() # backtrack before trying next neighbor
13
14 dfs(0)
15 return resultsTime: O(2^n ยท n) โ a complete DAG can have up to 2^(nโ2) distinct paths, and copying each path of length n costs O(n).
Space: O(2^n ยท n) for the stored results; the recursion stack grows at most O(n) deep since the graph is a DAG.
Complexity Summary
| Approach | Time | Space | When to use |
|---|---|---|---|
| DFS with Backtracking | O(2^n ยท n) | O(2^n ยท n) | The only viable approach โ enumerating all paths is inherently exponential |
Common Mistakes
- Storing a reference instead of a snapshot โ
results.append(current_path)adds the same mutable list every time; by the end, every entry points to an empty list. Useresults.append(list(current_path))to capture the state at that moment. - Adding a visited set โ because the graph is a DAG, no node can be revisited on any single path; a visited set wastes memory and introduces bugs if it mistakenly blocks legitimate paths shared by different routes.
- Forgetting to seed
current_pathwith node 0 โ if the initial call todfsstarts with an empty path, every recorded result will be missing the source node. - Hardcoding the destination index โ checking
node == n - 1with a separate variablenset outside the function is fragile; uselen(graph) - 1directly so the code is self-contained for any input. - Omitting the pop after recursion โ without the backtrack step, stale neighbors accumulate in
current_pathacross sibling branches, corrupting every path recorded after the first.
Related Problems
path-sum-iiโ same collect-all-paths-via-backtracking pattern, applied to a binary tree instead of a graphword-searchโ DFS with backtracking on a 2D grid where the path must be pruned and restored at each stepcourse-scheduleโ DFS on a directed adjacency list, but the goal is cycle detection rather than path enumerationnumber-of-islandsโ DFS/BFS graph exploration where reachability (not paths) is the objectiveclone-graphโ DFS traversal of an adjacency list to deeply copy an arbitrary graph structure