797. All Paths From Source to Target
Problem Description
You are given a directed acyclic graph (DAG) with n
nodes labeled from 0
to n - 1
. Your task is to find all possible paths that start from node 0
and end at node n - 1
. You can return these paths in any order.
The graph is represented as an adjacency list where graph[i]
contains a list of all nodes that can be reached directly from node i
. In other words, if node j
appears in graph[i]
, there is a directed edge from node i
to node j
.
For example:
- If
[graph](/problems/graph_intro) = [[1,2],[3],[3],[]]
, this represents a graph with 4 nodes (0, 1, 2, 3) - From node 0, you can go to nodes 1 and 2
- From node 1, you can go to node 3
- From node 2, you can go to node 3
- Node 3 has no outgoing edges
The paths from node 0 to node 3 would be: [[0,1,3], [0,2,3]]
Since the graph is a DAG, it has no cycles, which guarantees that all paths will eventually terminate and won't loop infinitely.
The solution uses a breadth-first search (BFS) approach with a queue to explore all possible paths. It starts with the path [0]
and keeps extending each path by adding neighboring nodes until reaching the target node n - 1
. When a path reaches the target node, it's added to the answer list. The algorithm continues until all possible paths have been explored.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem explicitly states we have a directed acyclic graph (DAG) with nodes and edges. The graph is represented as an adjacency list where
graph[i]
contains all nodes reachable from nodei
.
Is it a tree?
- No: While the graph is acyclic (being a DAG), it's not necessarily a tree. A tree has exactly one path between any two nodes, but in this problem, we're finding all possible paths from source to target, implying multiple paths can exist.
Is the problem related to directed acyclic graphs?
- Yes: The problem explicitly mentions we're dealing with a DAG and we need to find all paths from node 0 to node n-1.
Topological Sort
- While topological sort is often used with DAGs, this specific problem asks for all paths rather than an ordering of nodes. However, we need to continue evaluating...
Looking deeper at the problem requirements:
- We need to find all possible paths from source to target
- The graph has small constraints (typically n β€ 15 for such problems)
- We need to explore every possible route exhaustively
Does the problem have small constraints?
- Yes: Problems asking for "all possible paths" typically have small constraints since the number of paths can grow exponentially.
DFS/backtracking
- Yes: This is the appropriate approach. We need to explore all paths from source to target, which requires systematically visiting each possible route and backtracking when we reach dead ends or the target.
Conclusion: The flowchart guides us to use DFS (Depth-First Search) with backtracking. This makes sense because:
- We need to explore all complete paths from source to target
- DFS naturally maintains the current path as it traverses
- When we reach the target or a dead end, we backtrack to explore other branches
- The small constraints make this exhaustive search feasible
Intuition
When we need to find all possible paths from a source to a target in a graph, we essentially need to explore every route systematically. Think of it like exploring all possible ways to get from your home to a destination in a city where streets only go one way (since it's a directed graph).
The key insight is that we need to keep track of our current path as we explore. Starting from node 0
, we have multiple choices of where to go next. For each choice, we need to:
- Add that node to our current path
- Continue exploring from that new node
- Remember to explore all other choices as well
This naturally leads to a traversal approach where we maintain the path we're currently exploring. When we reach the target node n-1
, we've found a complete path and should save it. When we reach a dead end or finish exploring all neighbors of a node, we need to backtrack and try other options.
The solution uses BFS with a clever twist - instead of just storing nodes in the queue, it stores entire paths. Each element in the queue is a path (list of nodes) that we're currently exploring. For example:
- Start with path
[0]
- If node 0 connects to nodes 1 and 2, we create two new paths:
[0,1]
and[0,2]
- We continue extending each path until it reaches the target
This approach works because:
- The graph is acyclic (DAG), so we won't get stuck in infinite loops
- By storing complete paths in the queue, we automatically keep track of the route we took
- When a path's last node is
n-1
, we know we've found a complete source-to-target path - The BFS ensures we explore all possible paths systematically
While the provided solution uses BFS, DFS would work equally well here. The choice between BFS and DFS doesn't affect correctness since we need all paths anyway - both will explore the entire search space. The BFS approach with path tracking is just one clean way to implement this exhaustive search.
Learn more about Depth-First Search, Breadth-First Search, Graph and Backtracking patterns.
Solution Approach
The implementation uses a BFS approach with a queue to systematically explore all paths from source to target. Let's walk through the solution step by step:
Data Structures Used:
deque
: A double-ended queue to efficiently add and remove paths during BFS traversalans
: A list to store all valid paths from node 0 to node n-1
Algorithm Implementation:
-
Initialization:
n = len([graph](/problems/graph_intro)) q = deque([[0]]) ans = []
- Get the total number of nodes
n
from the graph - Initialize the queue with a single path
[0]
(starting from source node) - Create an empty answer list to store complete paths
- Get the total number of nodes
-
BFS Traversal:
while q: path = q.popleft() u = path[-1]
- Process paths in the queue one by one
- Extract the current path and get its last node
u
(the current position)
-
Check for Target:
if u == n - 1: ans.append(path) continue
- If the current node is the target (
n-1
), we've found a complete path - Add this path to our answer list
- Continue to the next path in the queue (no need to explore further from the target)
- If the current node is the target (
-
Path Extension:
for v in [graph](/problems/graph_intro)[u]: q.append(path + [v])
- If we haven't reached the target, explore all neighbors of the current node
- For each neighbor
v
, create a new path by appendingv
to the current path - Add this extended path to the queue for future exploration
Key Insights:
- By storing entire paths in the queue instead of just nodes, we automatically maintain the route taken to reach each node
- The
path + [v]
operation creates a new list, ensuring each path in the queue is independent - Since the graph is a DAG, we don't need to track visited nodes - there are no cycles to worry about
- The algorithm naturally terminates when all paths have been explored (queue becomes empty)
Time Complexity: O(2^N Γ N) in the worst case, where N is the number of nodes. In the worst case, there could be 2^N paths (if the graph is fully connected), and each path can have up to N nodes.
Space Complexity: O(2^N Γ N) for storing all the paths in the queue and the answer list.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with graph = [[1,2],[3],[3],[]]
.
This represents a graph with 4 nodes (0β1, 0β2, 1β3, 2β3), and we need to find all paths from node 0 to node 3.
Initial State:
- Queue:
[[0]]
- Answer:
[]
Iteration 1:
- Pop path
[0]
from queue - Current node is 0 (last element of path)
- Node 0 is not the target (3), so explore neighbors
- Node 0 has neighbors [1, 2]
- Add new paths to queue:
[0,1]
and[0,2]
- Queue:
[[0,1], [0,2]]
Iteration 2:
- Pop path
[0,1]
from queue - Current node is 1
- Node 1 is not the target, so explore neighbors
- Node 1 has neighbor [3]
- Add new path to queue:
[0,1,3]
- Queue:
[[0,2], [0,1,3]]
Iteration 3:
- Pop path
[0,2]
from queue - Current node is 2
- Node 2 is not the target, so explore neighbors
- Node 2 has neighbor [3]
- Add new path to queue:
[0,2,3]
- Queue:
[[0,1,3], [0,2,3]]
Iteration 4:
- Pop path
[0,1,3]
from queue - Current node is 3
- Node 3 IS the target! Add path to answer
- Answer:
[[0,1,3]]
- Queue:
[[0,2,3]]
Iteration 5:
- Pop path
[0,2,3]
from queue - Current node is 3
- Node 3 IS the target! Add path to answer
- Answer:
[[0,1,3], [0,2,3]]
- Queue:
[]
Result:
Queue is empty, algorithm terminates. We found all paths: [[0,1,3], [0,2,3]]
Solution Implementation
1from collections import deque
2from typing import List
3
4class Solution:
5 def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]:
6 """
7 Find all possible paths from node 0 to node n-1 in a directed acyclic graph.
8
9 Args:
10 graph: Adjacency list representation where graph[i] contains all nodes that node i connects to
11
12 Returns:
13 List of all paths from source (0) to target (n-1)
14 """
15 # Get the number of nodes in the graph
16 n = len(graph)
17
18 # Initialize queue with the starting path containing only node 0
19 queue = deque([[0]])
20
21 # Store all valid paths from source to target
22 result = []
23
24 # BFS traversal to explore all paths
25 while queue:
26 # Get the current path from the queue
27 current_path = queue.popleft()
28
29 # Get the last node in the current path
30 last_node = current_path[-1]
31
32 # Check if we've reached the target node (n-1)
33 if last_node == n - 1:
34 # Add the complete path to results
35 result.append(current_path)
36 continue
37
38 # Explore all neighbors of the current node
39 for neighbor in graph[last_node]:
40 # Create a new path by extending the current path with the neighbor
41 new_path = current_path + [neighbor]
42 queue.append(new_path)
43
44 return result
45
1class Solution {
2 /**
3 * Find all possible paths from node 0 to node n-1 in a directed acyclic graph.
4 * Uses BFS approach to explore all paths.
5 *
6 * @param graph Adjacency list representation where graph[i] contains all nodes that node i connects to
7 * @return List of all paths from source (0) to target (n-1)
8 */
9 public List<List<Integer>> allPathsSourceTarget(int[][] graph) {
10 int targetNode = graph.length - 1;
11
12 // Queue to store paths being explored (each path is a list of nodes)
13 Queue<List<Integer>> pathQueue = new ArrayDeque<>();
14
15 // Start with initial path containing only the source node (0)
16 pathQueue.offer(Arrays.asList(0));
17
18 // Store all valid paths from source to target
19 List<List<Integer>> allPaths = new ArrayList<>();
20
21 // BFS traversal to explore all possible paths
22 while (!pathQueue.isEmpty()) {
23 List<Integer> currentPath = pathQueue.poll();
24
25 // Get the last node in the current path
26 int lastNode = currentPath.get(currentPath.size() - 1);
27
28 // If we've reached the target node, add this path to results
29 if (lastNode == targetNode) {
30 allPaths.add(currentPath);
31 continue;
32 }
33
34 // Explore all neighbors of the current node
35 for (int neighbor : graph[lastNode]) {
36 // Create a new path by extending the current path
37 List<Integer> extendedPath = new ArrayList<>(currentPath);
38 extendedPath.add(neighbor);
39
40 // Add the extended path to queue for further exploration
41 pathQueue.offer(extendedPath);
42 }
43 }
44
45 return allPaths;
46 }
47}
48
1class Solution {
2public:
3 vector<vector<int>> graph;
4 vector<vector<int>> allPaths;
5
6 vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
7 // Store the graph as a member variable for access in DFS
8 this->graph = graph;
9
10 // Initialize path with starting node 0
11 vector<int> currentPath;
12 currentPath.push_back(0);
13
14 // Start DFS traversal from node 0
15 dfs(0, currentPath);
16
17 return allPaths;
18 }
19
20private:
21 void dfs(int currentNode, vector<int>& currentPath) {
22 // Check if we've reached the target node (last node in graph)
23 if (currentNode == graph.size() - 1) {
24 // Add the complete path to our result
25 allPaths.push_back(currentPath);
26 return;
27 }
28
29 // Explore all neighbors of the current node
30 for (int neighbor : graph[currentNode]) {
31 // Add neighbor to current path
32 currentPath.push_back(neighbor);
33
34 // Recursively explore from this neighbor
35 dfs(neighbor, currentPath);
36
37 // Backtrack: remove neighbor from path to explore other possibilities
38 currentPath.pop_back();
39 }
40 }
41};
42
1/**
2 * Finds all possible paths from node 0 to node n-1 in a directed acyclic graph
3 * @param graph - Adjacency list representation where graph[i] contains all nodes that node i connects to
4 * @returns All paths from source (node 0) to target (node n-1)
5 */
6function allPathsSourceTarget(graph: number[][]): number[][] {
7 // Store all valid paths from source to target
8 const allPaths: number[][] = [];
9
10 /**
11 * Depth-first search to explore all paths
12 * @param currentPath - The path being constructed from source node
13 */
14 const depthFirstSearch = (currentPath: number[]): void => {
15 // Get the last node in the current path
16 const currentNode: number = currentPath[currentPath.length - 1];
17
18 // Check if we've reached the target node (last node in graph)
19 if (currentNode === graph.length - 1) {
20 // Found a complete path, add a copy to results
21 allPaths.push([...currentPath]);
22 return;
23 }
24
25 // Explore all neighbors of the current node
26 for (const neighbor of graph[currentNode]) {
27 // Add neighbor to current path
28 currentPath.push(neighbor);
29
30 // Recursively explore from this neighbor
31 depthFirstSearch(currentPath);
32
33 // Backtrack: remove neighbor to explore other possibilities
34 currentPath.pop();
35 }
36 };
37
38 // Start DFS from node 0
39 depthFirstSearch([0]);
40
41 return allPaths;
42}
43
Time and Space Complexity
Time Complexity: O(2^N * N)
The algorithm performs a BFS traversal to find all paths from node 0 to node N-1 in a directed acyclic graph (DAG). In the worst case, we have a complete DAG where every node has edges to all subsequent nodes. This creates the maximum number of paths.
- The maximum number of paths from source to target in a DAG can be
2^(N-2)
(considering nodes between source and target). - For each path discovered, we create a new list by concatenating the current path with a new node, which takes
O(N)
time in the worst case (when the path length is N). - Each node can be visited multiple times as part of different paths.
- Therefore, the total time complexity is
O(2^N * N)
.
Space Complexity: O(2^N * N)
The space complexity consists of:
- Queue space: In the worst case, the queue can hold all possible partial paths simultaneously. The maximum number of paths that can exist is
O(2^N)
, and each path can have up toN
nodes, resulting inO(2^N * N)
space. - Answer list: The answer list stores all valid paths from source to target. In the worst case, there can be
O(2^N)
such paths, each of length up toN
, requiringO(2^N * N)
space. - Path copies: When we do
path + [v]
, we create a new list, but this is already accounted for in the queue space analysis.
The dominant factor is the storage of all paths, giving us a total space complexity of O(2^N * N)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Mutating the Same Path Object
One of the most common mistakes is reusing the same path list object when extending paths, which leads to all paths in the queue pointing to the same list.
Incorrect Implementation:
# WRONG: This modifies the same path object for v in graph[u]: path.append(v) # Mutates the original path q.append(path) # All queue entries point to the same list path.pop() # Trying to backtrack, but damage is already done
Why it fails: When you append the same list object to the queue multiple times, all references point to the same list in memory. Any modification affects all "copies" in the queue.
Correct Solution:
# RIGHT: Create a new path for each neighbor for v in graph[u]: q.append(path + [v]) # Creates a new list object
2. Using DFS with Incorrect Backtracking
When implementing this with DFS instead of BFS, forgetting to properly backtrack the path state is a common error.
Incorrect DFS Implementation:
def dfs(node, path):
if node == n - 1:
ans.append(path) # WRONG: Appending reference to mutable path
return
for neighbor in graph[node]:
path.append(neighbor)
dfs(neighbor, path)
# Forgot to backtrack: path.pop()
Correct DFS Solution:
def dfs(node, path):
if node == n - 1:
ans.append(path[:]) # Create a copy of the path
return
for neighbor in graph[node]:
path.append(neighbor)
dfs(neighbor, path)
path.pop() # Proper backtracking
3. Memory Inefficiency with Large Graphs
Storing complete paths in the queue can consume excessive memory for graphs with many paths.
Problem Scenario:
- In a densely connected DAG, the number of paths can grow exponentially
- Each path is stored as a separate list in the queue
- This can lead to memory overflow for large graphs
Optimized Alternative Using DFS:
class Solution:
def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]:
def dfs(node, path):
# Process one path at a time, reducing memory footprint
if node == len(graph) - 1:
result.append(path)
return
for neighbor in graph[node]:
dfs(neighbor, path + [neighbor])
result = []
dfs(0, [0])
return result
This DFS approach uses the call stack instead of explicitly storing all partial paths, which can be more memory-efficient in practice.
What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Want a Structured Path to Master System Design Too? Donβt Miss This!