Last Updated: March 28, 2026
We have a directed acyclic graph represented as an adjacency list, and we need to find every possible path from node 0 to node n - 1. The key word here is "all" paths, not the shortest or longest, but every single one.
Since the graph is a DAG, there are no cycles. This is important because it means we will never get stuck in an infinite loop during traversal. Every path we explore is guaranteed to terminate. Without cycles, we also do not need a "visited" set to prevent revisiting nodes. In fact, we must NOT use a global visited set, because the same node can appear in multiple valid paths.
The core observation is that this is a classic backtracking problem on a graph. We start at node 0, explore each neighbor, and when we reach node n - 1, we record the current path. Because the graph is acyclic, every recursive call eventually bottoms out.
2 <= n <= 15 -> The graph is very small. With at most 15 nodes, even exponential algorithms are fine. In a DAG with n nodes, the maximum number of paths can be exponential (up to 2^(n-2) in a fully connected DAG), but with n = 15, that is at most 8192 paths.graph[i][j] != i -> No self-loops, confirming it is a proper DAG.The input graph is guaranteed to be a DAG -> No cycles, so DFS will always terminate without needing a visited set.The most natural way to find all paths in a graph is depth-first search. Start at node 0, pick a neighbor, go deeper, pick another neighbor, go deeper again, and so on. When you reach node n - 1, you have found a complete path. When you hit a dead end or finish exploring a branch, backtrack and try the next neighbor.
Because the graph is a DAG, we do not need a visited set. There are no cycles, so we will never loop back to a node we are currently visiting. This simplifies the code quite a bit. We just need to maintain a "current path" list, add nodes as we go deeper, and remove them when we backtrack.
0 with a current path containing just [0].n - 1. If yes, add a copy of the current path to the result.This approach recomputes sub-paths from shared intermediate nodes. What if we explored paths layer by layer using BFS instead?
Instead of exploring depth-first, we can use breadth-first search. The twist is that a normal BFS only tracks which nodes to visit next. Here, we need to track the entire path leading to each node, because we need to output complete paths, not just reachability.
So each entry in the BFS queue is not just a node, but a full path from node 0 to that node. When we dequeue a path whose last node is the target, we add it to the result. Otherwise, we extend the path by appending each neighbor and enqueueing the extended paths.
[0].n - 1, add the path to the result.BFS uses significantly more memory since it stores full path copies in the queue. What if we combined DFS with caching to avoid recomputing sub-paths from the same intermediate node?
Here is the key insight: in a DAG, the set of paths from any intermediate node X to the target is always the same, regardless of how we arrived at X. If nodes 1 and 2 both have edges to node 3, the paths from node 3 to the target are identical in both cases. The only difference is the prefix (how we got to node 3).
So instead of recomputing paths from node 3 to the target every time we visit it, we can cache them. The first time we visit node 3, we compute all paths from 3 to the target and store them. On subsequent visits, we just prepend the current prefix to each cached sub-path.
This is a top-down dynamic programming approach on the DAG. Since the graph is acyclic, there is a natural topological ordering, and memoization is guaranteed to work without circular dependencies.
In a DAG, the paths from any node X to the target form a fixed set that does not depend on how we reached X. This is the overlapping subproblems property that makes memoization effective. By caching allPaths(X) the first time we compute it, subsequent calls to allPaths(X) from other parent nodes return instantly.
The memoization does not change the worst-case time complexity, because we still have to construct and output every path. But in practice, it avoids redundant DFS traversals. If node X is reachable from k different parent nodes, plain DFS would traverse the entire subgraph rooted at X a total of k times. With memoization, we traverse it once and then reuse the cached sub-paths k-1 times.
allPaths(node) that returns all paths from node to the target.node is the target, return [[target]].node is already in the memo, return the cached result.node, recursively get all paths from neighbor to the target. For each such path, prepend node to it and add it to the result.allPaths(0) to get the final answer.