All Paths From Source to Target
Problem
Given a directed acyclic graph (DAG) represented as an adjacency list, return all possible paths from node 0 to node n-1, where n is the number of nodes in the graph.
- 2 ≤ graph.length ≤ 15
- 0 ≤ graph[i][j] < graph.length
- The graph is guaranteed to be acyclic.
Example
graph = [[1,2],[3],[3],[]][[0,1,3],[0,2,3]]Starting at node 0, the algorithm explores neighbors 1 and 2. From 1, it moves to 3, reaching the target and recording path [0,1,3]. From 2, it also moves to 3, recording path [0,2,3]. The DFS explores all paths exhaustively, backtracking after each complete path is found to explore alternative routes.
Approach
Straightforward Solution
A brute-force approach would attempt to generate all possible sequences of nodes, but without pruning or structure, this could be inefficient and complex to implement.
Core Observation
The problem requires enumerating all paths from a start node to an end node in a DAG. This naturally models a stateful exploration where each path is a sequence of nodes, and the graph's acyclic property guarantees no infinite loops.
Path to Optimal
Recognizing the graph is acyclic allows the use of Depth-First Search (DFS) with backtracking to systematically explore all paths. DFS explores each path fully before backtracking, ensuring all paths are found without revisiting nodes in cycles. The backtracking step restores the path state after exploring each neighbor, enabling exploration of alternative paths.
Optimal Approach
Implement a recursive DFS starting from node 0, maintaining the current path. When the target node is reached, append a copy of the path to the results. For each neighbor, append it to the path, recurse, then remove it (backtrack). This approach enumerates all paths efficiently and correctly.
Time
O(2^n * n)
In the worst case, the number of paths can be exponential in the number of nodes, and each path can be up to length n. The algorithm explores all paths, copying each to the result.
Space
O(n)
The recursion stack and path list use O(n) space, where n is the number of nodes. The output space is not counted as auxiliary space.
Pattern Spotlight
DFS (Stateful Exploration with Backtracking)
When enumerating all paths in a DAG, use DFS to explore each path fully, and backtrack by undoing the last step after recursion returns to restore state for alternative explorations.
Solution
| 1 | class Solution: |
| 2 | def allPathsSourceTarget(self, graph: list[list[int]]) -> list[list[int]]: |
| 3 | target = len(graph) - 1 |
| 4 | res = [] |
| 5 | |
| 6 | def dfs(node, path): |
| 7 | if node == target: |
| 8 | res.append(path[:]) |
| 9 | return |
| 10 | |
| 11 | for nei in graph[node]: |
| 12 | path.append(nei) |
| 13 | dfs(nei, path) |
| 14 | path.pop() |
| 15 | |
| 16 | dfs(0, [0]) |
| 17 | return res |
Step-by-Step Solution
Initialize Target Node and Result Container
| 3 | target = len(graph) - 1 |
| 4 | res = [] |
Objective
To define the target node index and prepare a list to collect all valid paths.
Key Insight
Identifying the target node upfront simplifies the base case check in DFS. The result list accumulates all complete paths found during traversal, enabling a clean separation between exploration and storage.
Interview Quick-Check
Core Logic
Defining the target as `len(graph) - 1` allows the DFS to know when a path is complete.
State & Boundaries
The result list must be initialized before DFS to collect all paths found.
Explore All Paths Recursively Using DFS with Backtracking
| 6 | def dfs(node, path): |
| 7 | if node == target: |
| 8 | res.append(path[:]) |
| 9 | return |
| 11 | for nei in graph[node]: |
| 12 | path.append(nei) |
| 13 | dfs(nei, path) |
| 14 | path.pop() |
Objective
To recursively traverse the graph from the current node, building paths and backtracking after exploring each neighbor.
Key Insight
DFS explores each path fully by appending neighbors to the current path and recursing. When the target is reached, the current path is copied and saved. Backtracking by popping the last node after recursion restores the path state, enabling exploration of alternative neighbors without interference.
Interview Quick-Check
Core Logic
DFS with backtracking systematically explores all paths by extending the path with neighbors, recursing, and then undoing the extension to explore other branches.
State & Boundaries
The base case is when the current node equals the target, at which point the current path is a complete valid path.
Common Pitfalls & Bugs
Failing to backtrack (pop the last node) after recursion leads to incorrect paths that include nodes from previous branches.
Start DFS from Source Node and Return All Paths
| 16 | dfs(0, [0]) |
| 17 | return res |
Objective
To initiate the DFS traversal from node 0 with the initial path and return the collected paths after exploration.
Key Insight
Starting DFS with the initial path containing only the source node sets the foundation for path building. Returning the result after DFS completes provides the full set of paths from source to target.
Interview Quick-Check
Core Logic
The initial DFS call seeds the recursion with the source node and an initial path.
State & Boundaries
Returning the result after DFS completes ensures all paths have been explored and collected.
Line Analysis
This solution has 4 Critical lines interviewers watch for.
res.append(path[:])
Append a copy of the current path to the result list.
Copying the path is essential because the path list is mutable and will be modified during backtracking; storing a copy preserves the current valid path.
path.pop()
Remove the last node from the path to backtrack after recursion.
Backtracking restores the path state to explore alternative neighbors without interference from previous recursive calls, which is critical for correctness.
if node == target:
Check if the current node is the target node.
This base case identifies when a complete path has been found and triggers storing the path.
dfs(nei, path)
Recursively call DFS on the neighbor node with the updated path.
This recursive call explores all paths starting from the neighbor, enabling depth-first traversal of the graph.
return
Return from the DFS call after recording a complete path.
Returning prevents further exploration from the target node, correctly terminating this path's recursion branch.
path.append(nei)
Add the neighbor node to the current path before recursion.
Extending the path with the neighbor represents moving forward in the graph and is necessary for building complete paths.
dfs(0, [0])
Start DFS traversal from the source node with the initial path containing only node 0.
Seeding the recursion with the source node and initial path sets the foundation for exploring all paths from source to target.
target = len(graph) - 1
Calculate the target node index as the last node in the graph.
Defining the target node upfront simplifies the base case check in the DFS recursion, enabling a clear stopping condition.
def dfs(node, path):
Define the recursive DFS function that explores paths from the current node.
Encapsulating the traversal logic in a recursive function allows systematic exploration of all paths with stateful path tracking.
for nei in graph[node]:
Iterate over all neighbors of the current node.
Exploring each neighbor is necessary to traverse all possible paths from the current node.
return res
Return the list of all paths found from source to target.
Returning the accumulated results after DFS completes provides the final answer to the problem.
res = []
Initialize an empty list to store all valid paths from source to target.
This list accumulates all complete paths found during DFS, separating exploration from result storage.
Test Your Understanding
Why is backtracking (removing the last node from the path after recursion) essential in this DFS approach?
Backtracking restores the path state after exploring a neighbor, ensuring that subsequent recursive calls explore alternative paths correctly without carrying over nodes from previous paths.
Related Problems
DFS pattern
Don't just read it. Drill it.
Reconstruct All Paths From Source to Target from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.
Unlock the All Paths From Source to Target drill