title: All Paths From Source to Target
slug: all-paths-from-source-to-target
difficulty: medium
leetcode_id: 797
leetcode_url: https://leetcode.com/problems/all-paths-from-source-to-target/
categories:
  - graphs
  - recursion
patterns:
  - slug: dfs
    is_optimal: true
  - slug: backtracking
    is_optimal: false

function_signature: "def all_paths_source_target(graph: list[list[int]]) -> list[list[int]]:"

test_cases:
  visible:
    - input: { graph: [[1, 2], [3], [3], []] }
      expected: [[0, 1, 3], [0, 2, 3]]
    - input: { graph: [[4, 3, 1], [3, 2, 4], [3], [4], []] }
      expected: [[0, 4], [0, 3, 4], [0, 1, 3, 4], [0, 1, 2, 3, 4], [0, 1, 4]]
  hidden:
    - input: { graph: [[1], []] }
      expected: [[0, 1]]
    - input: { graph: [[1, 2, 3], [4], [4], [4], []] }
      expected: [[0, 1, 4], [0, 2, 4], [0, 3, 4]]
    - input: { graph: [[2, 1], [2], []] }
      expected: [[0, 2], [0, 1, 2]]
    - input: { graph: [[1, 3], [2], [3], []] }
      expected: [[0, 1, 2, 3], [0, 3]]

description: |
  Given a directed acyclic graph (**DAG**) of `n` nodes labeled from `0` to `n - 1`, find all possible paths from node `0` to node `n - 1` and return them in **any order**.

  The graph is given as follows: `graph[i]` is a list of all nodes you can visit from node `i` (i.e., there is a directed edge from node `i` to node `graph[i][j]`).

constraints: |
  - `n == graph.length`
  - `2 <= n <= 15`
  - `0 <= graph[i][j] < n`
  - `graph[i][j] != i` (no self-loops)
  - All elements of `graph[i]` are **unique**
  - The input graph is **guaranteed** to be a **DAG**

examples:
  - input: "graph = [[1,2],[3],[3],[]]"
    output: "[[0,1,3],[0,2,3]]"
    explanation: "There are two paths: 0 -> 1 -> 3 and 0 -> 2 -> 3."
  - input: "graph = [[4,3,1],[3,2,4],[3],[4],[]]"
    output: "[[0,4],[0,3,4],[0,1,3,4],[0,1,2,3,4],[0,1,4]]"
    explanation: "There are five different paths from node 0 to node 4."

explanation:
  intuition: |
    Imagine you're standing at the entrance of a maze (node `0`) and need to find **every possible route** to the exit (node `n-1`). Unlike finding the shortest path, where you'd stop after reaching the destination once, here you need to explore *all* branches systematically.

    The key insight is that the graph is a **DAG** (Directed Acyclic Graph) — there are no cycles. This is crucial because it means:
    1. You can never get stuck in an infinite loop
    2. Every path you start will eventually either reach the target or hit a dead end
    3. You don't need to track "visited" nodes globally (a node can appear in multiple valid paths)

    Think of it like exploring a family tree from an ancestor to all descendants named "Target". You start at the root, follow each branch completely, record the path when you find a Target, then *backtrack* to explore other branches.

    The **backtracking** pattern is perfect here: build a path incrementally, and when you reach the destination (or a dead end), undo your last choice and try a different branch.

  approach: |
    We solve this using **Depth-First Search with Backtracking**:

    **Step 1: Set up the recursive DFS function**

    - Create a helper function `dfs(node, path)` that explores from the current node
    - `path` is a list tracking the nodes visited so far in the current exploration

    &nbsp;

    **Step 2: Handle the base case**

    - If `node == n - 1` (we've reached the target), we found a complete path
    - Add a *copy* of the current path to our results list
    - Important: We must copy because the same list object will be modified during backtracking

    &nbsp;

    **Step 3: Explore all neighbors**

    - For each neighbor in `graph[node]`:
      - Add the neighbor to our current path
      - Recursively call `dfs(neighbor, path)` to continue exploring
      - **Backtrack**: Remove the neighbor from the path after the recursive call returns

    &nbsp;

    **Step 4: Start the traversal**

    - Begin DFS from node `0` with the initial path `[0]`
    - Return the collected results after all paths are explored

    &nbsp;

    The backtracking step (removing the neighbor after recursion) is what allows us to explore *all* branches — we "undo" our choice so we can try other neighbors.

  common_pitfalls:
    - title: Forgetting to Copy the Path
      description: |
        When you find a valid path and add it to results, you must add a **copy** of the list, not the list itself.

        ```python
        # Wrong - all paths in results will be the same (empty) list
        results.append(path)

        # Correct - creates an independent copy
        results.append(path[:])  # or list(path)
        ```

        Since we're backtracking and modifying `path` in place, if you don't copy, all entries in `results` will reference the same list object — which ends up empty after all backtracking completes.
      wrong_approach: "results.append(path)"
      correct_approach: "results.append(path[:]) or results.append(list(path))"

    - title: Tracking Visited Nodes Globally
      description: |
        In many graph problems, you track visited nodes to avoid infinite loops. Here, that would be **incorrect**.

        Because this is a DAG, we're guaranteed no cycles, so infinite loops aren't possible. More importantly, the same node can legitimately appear in *multiple different paths*. If you mark it as "visited" globally, you'd miss valid paths.

        For example, in `graph = [[1,2],[3],[3],[]]`, node `3` is reachable via both `0->1->3` and `0->2->3`. A global visited set would prevent finding the second path.
      wrong_approach: "Global visited set preventing revisits"
      correct_approach: "No visited tracking needed — DAG guarantees no cycles"

    - title: Not Backtracking After Recursion
      description: |
        If you add a node to the path but forget to remove it after the recursive call, your paths will contain nodes from other branches.

        ```python
        # Wrong - path keeps growing, contains nodes from all branches
        for neighbor in graph[node]:
            path.append(neighbor)
            dfs(neighbor)
            # Missing: path.pop()

        # Correct - remove after exploring
        for neighbor in graph[node]:
            path.append(neighbor)
            dfs(neighbor)
            path.pop()  # Backtrack
        ```
      wrong_approach: "Append without pop"
      correct_approach: "Always pop after recursive call returns"

  key_takeaways:
    - "**Backtracking pattern**: Build solution incrementally, undo choices after exploring to try alternatives"
    - "**DAG property**: No cycles means no need for visited tracking — the same node can appear in multiple valid paths"
    - "**Copy on record**: When storing a path, always copy the list to avoid reference issues during backtracking"
    - "**Foundation for path enumeration**: This technique extends to finding all paths in trees, counting paths, or finding paths with specific properties"

  time_complexity: "O(2^n * n). In the worst case (complete DAG), there can be `2^(n-2)` paths, and each path can have up to `n` nodes to copy."
  space_complexity: "O(n). The recursion stack depth is at most `n` (longest path), and the current path stores at most `n` nodes. Output space is not counted."

solutions:
  - approach_name: DFS with Backtracking
    is_optimal: true
    code: |
      def all_paths_source_target(graph: list[list[int]]) -> list[list[int]]:
          target = len(graph) - 1
          results = []

          def dfs(node: int, path: list[int]) -> None:
              # Base case: reached the target node
              if node == target:
                  # Important: append a COPY of the path
                  results.append(path[:])
                  return

              # Explore all neighbors
              for neighbor in graph[node]:
                  path.append(neighbor)  # Choose
                  dfs(neighbor, path)    # Explore
                  path.pop()             # Unchoose (backtrack)

          # Start DFS from node 0
          dfs(0, [0])
          return results
    explanation: |
      **Time Complexity:** O(2^n * n) — In a complete DAG, there can be exponentially many paths, and we copy each path (length up to n) when recording.

      **Space Complexity:** O(n) — Recursion stack depth and current path length are both bounded by n.

      The classic backtracking template: choose (add to path), explore (recurse), unchoose (remove from path). The DAG property guarantees termination without needing a visited set.

  - approach_name: BFS with Path Tracking
    is_optimal: false
    code: |
      from collections import deque

      def all_paths_source_target(graph: list[list[int]]) -> list[list[int]]:
          target = len(graph) - 1
          results = []

          # Queue holds (current_node, path_so_far)
          queue = deque([(0, [0])])

          while queue:
              node, path = queue.popleft()

              if node == target:
                  results.append(path)
                  continue

              # Add all neighbors with extended paths
              for neighbor in graph[node]:
                  # Create new path for each branch
                  queue.append((neighbor, path + [neighbor]))

          return results
    explanation: |
      **Time Complexity:** O(2^n * n) — Same as DFS; we still explore all paths.

      **Space Complexity:** O(2^n * n) — Queue can hold many partial paths simultaneously, each up to length n.

      BFS explores level by level. We store the entire path with each queue entry, creating new lists for each branch. This uses more memory than DFS backtracking but avoids recursion. Less elegant for this problem but useful when you need shortest paths first.
