Facebook Pixel

1059. All Paths from Source Lead to Destination πŸ”’

Problem Description

You are given a directed graph with n nodes (labeled from 0 to n-1) represented by an edge list where edges[i] = [ai, bi] means there is a directed edge from node ai to node bi. You are also given two specific nodes: source and destination.

Your task is to determine if all paths starting from source eventually lead to destination.

For this to be true, three conditions must be satisfied:

  1. At least one path exists from source to destination
  2. Any terminal node must be the destination: If you can reach a node that has no outgoing edges (a dead end), that node must be destination
  3. No infinite loops: The number of possible paths from source to destination must be finite (meaning there are no cycles that can be reached from source)

Return true if all paths from source lead to destination, otherwise return false.

Example scenarios:

  • If there's a path from source that reaches a dead-end node other than destination, return false
  • If there's a cycle reachable from source, return false (as this creates infinite paths)
  • If all paths from source eventually reach destination and nowhere else, return true
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

To check if all paths from source lead to destination, we need to explore every possible path starting from source and verify that each one satisfies our conditions.

The key insight is that we can use depth-first search (DFS) to traverse all paths from the source. During our traversal, we need to handle three critical scenarios:

  1. Reaching the destination: When we reach the destination node, we need to verify it's a valid terminal point. The destination should have no outgoing edges (it should be a dead end), otherwise paths could continue beyond it.

  2. Detecting cycles: If we encounter a node we're currently visiting in our DFS path (a node in our current recursion stack), we've found a cycle. This violates the finite paths requirement, so we should return false.

  3. Finding other dead ends: If we reach a node with no outgoing edges that isn't the destination, we've found a path that doesn't lead to our target, so we should return false.

The algorithm naturally emerges from these observations:

  • We maintain a visited set to track nodes in our current DFS path (to detect cycles)
  • For each node, we recursively check all its neighbors
  • We only return true if all recursive calls from a node return true
  • We use memoization (@cache) to avoid recomputing results for nodes we've already fully explored

The beauty of this approach is that DFS naturally explores all possible paths, and by checking our three conditions at each step, we can determine if every path from source eventually leads to destination.

Learn more about Graph and Topological Sort patterns.

Solution Approach

The solution implements a DFS-based approach with memoization to efficiently check all paths from the source node.

Data Structures Used:

  • g: A dictionary (defaultdict) to store the adjacency list representation of the graph
  • vis: A set to track nodes currently being visited in the DFS path (for cycle detection)
  • @cache: Decorator for memoization to avoid recomputing results for already processed nodes

Implementation Steps:

  1. Build the Graph:

    g = defaultdict(list)
    for a, b in edges:
        g[a].append(b)

    We convert the edge list into an adjacency list for efficient traversal.

  2. DFS Function with Memoization:

    @cache
    def dfs(i):

    The dfs function recursively explores all paths from node i. The @cache decorator ensures we don't recompute results for nodes we've already fully explored.

  3. Base Cases:

    • Reached destination:

      if i == destination:
          return not g[i]

      If we reach the destination, we return True only if it has no outgoing edges (it's a terminal node).

    • Cycle detection or dead end:

      if i in vis or not g[i]:
          return False

      If the node is already in our current path (i in vis), we've found a cycle. If the node has no outgoing edges (not g[i]) and it's not the destination, it's an invalid dead end.

  4. Recursive Exploration:

    vis.add(i)
    for j in g[i]:
        if not dfs(j):
            return False
    return True
    • Add the current node to the visited set
    • Recursively check all neighbors
    • If any neighbor path returns False, the current path is invalid
    • If all neighbor paths are valid, return True
  5. Main Execution:

    vis = set()
    return dfs(source)

    Initialize the visited set and start DFS from the source node.

The algorithm correctly handles all three requirements: it finds paths to destination, detects cycles (infinite paths), and identifies invalid terminal nodes. The time complexity is O(V + E) where V is the number of vertices and E is the number of edges, as we visit each node and edge at most once due to memoization.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate the solution approach.

Example Graph:

edges = [[0,1], [0,2], [1,3], [2,3]]
source = 0
destination = 3

This creates the following graph:

    0
   / \
  1   2
   \ /
    3

Step-by-step DFS execution:

  1. Start: dfs(0)

    • Node 0 is not the destination
    • Node 0 is not in vis and has outgoing edges [1, 2]
    • Add 0 to vis: vis = {0}
    • Need to check dfs(1) and dfs(2)
  2. First branch: dfs(1)

    • Node 1 is not the destination
    • Node 1 is not in vis and has outgoing edges [3]
    • Add 1 to vis: vis = {0, 1}
    • Need to check dfs(3)
  3. Reach destination: dfs(3)

    • Node 3 is the destination
    • Check if node 3 has outgoing edges: g[3] = [] (empty)
    • Since destination has no outgoing edges, return True
    • Remove 1 from vis (backtrack): vis = {0}
  4. Second branch: dfs(2)

    • Node 2 is not the destination
    • Node 2 is not in vis and has outgoing edges [3]
    • Add 2 to vis: vis = {0, 2}
    • Need to check dfs(3)
  5. Reach destination again: dfs(3)

    • Due to @cache, we don't recompute - immediately return True
    • Remove 2 from vis (backtrack): vis = {0}
  6. Final result:

    • Both paths from node 0 (0β†’1β†’3 and 0β†’2β†’3) lead to destination
    • Destination has no outgoing edges (valid terminal)
    • No cycles detected
    • Return True

Counter-example with a cycle:

edges = [[0,1], [1,2], [2,1], [1,3]]
source = 0
destination = 3

This creates:

    0 β†’ 1 ⟷ 2
        ↓
        3

When we reach node 2, it points back to node 1. If we follow 2β†’1, node 1 is already in vis, indicating a cycle. The function would return False because infinite paths exist (we can loop 1β†’2β†’1β†’2... forever).

Solution Implementation

1from typing import List
2from collections import defaultdict
3from functools import cache
4
5class Solution:
6    def leadsToDestination(
7        self, n: int, edges: List[List[int]], source: int, destination: int
8    ) -> bool:
9        """
10        Check if all paths starting from source eventually lead to destination.
11      
12        Args:
13            n: Number of nodes in the graph
14            edges: List of directed edges [from_node, to_node]
15            source: Starting node
16            destination: Target destination node
17      
18        Returns:
19            True if all paths from source lead to destination, False otherwise
20        """
21      
22        # Build adjacency list representation of the graph
23        graph = defaultdict(list)
24        for from_node, to_node in edges:
25            graph[from_node].append(to_node)
26      
27        # Track visited nodes to detect cycles
28        visited = set()
29      
30        @cache
31        def dfs(current_node: int) -> bool:
32            """
33            Depth-first search to check if all paths from current node lead to destination.
34          
35            Args:
36                current_node: Current node being explored
37          
38            Returns:
39                True if all paths from current_node lead to destination
40            """
41            # Base case: reached destination
42            if current_node == destination:
43                # Destination should have no outgoing edges (terminal node)
44                return len(graph[current_node]) == 0
45          
46            # Check for cycle or dead-end that's not the destination
47            if current_node in visited or len(graph[current_node]) == 0:
48                return False
49          
50            # Mark current node as visited to detect cycles
51            visited.add(current_node)
52          
53            # Explore all neighbors
54            for neighbor in graph[current_node]:
55                if not dfs(neighbor):
56                    return False
57          
58            # All paths from this node lead to destination
59            return True
60      
61        # Start DFS from source node
62        return dfs(source)
63
1class Solution {
2    // Adjacency list representation of the graph
3    private List<Integer>[] adjacencyList;
4    // Memoization array: 1 = valid path, -1 = invalid path, 0 = not computed
5    private int[] memo;
6    // Visited array to detect cycles during DFS
7    private boolean[] visited;
8    // Target destination node
9    private int destination;
10
11    public boolean leadsToDestination(int n, int[][] edges, int source, int destination) {
12        // Initialize arrays
13        this.visited = new boolean[n];
14        this.adjacencyList = new List[n];
15        this.destination = destination;
16        this.memo = new int[n];
17      
18        // Initialize adjacency list for each node
19        Arrays.setAll(adjacencyList, index -> new ArrayList<>());
20      
21        // Build the directed graph from edges
22        for (int[] edge : edges) {
23            adjacencyList[edge[0]].add(edge[1]);
24        }
25      
26        // Start DFS from source node
27        return dfs(source);
28    }
29
30    private boolean dfs(int currentNode) {
31        // Base case: if we reached destination, check if it has no outgoing edges
32        if (currentNode == destination) {
33            return adjacencyList[currentNode].isEmpty();
34        }
35      
36        // If already computed, return the memoized result
37        if (memo[currentNode] != 0) {
38            return memo[currentNode] == 1;
39        }
40      
41        // Check for cycle (node is in current path) or dead-end (non-destination leaf node)
42        if (visited[currentNode] || adjacencyList[currentNode].isEmpty()) {
43            return false;
44        }
45      
46        // Mark current node as visited in the current path
47        visited[currentNode] = true;
48      
49        // Explore all neighbors
50        for (int neighbor : adjacencyList[currentNode]) {
51            if (!dfs(neighbor)) {
52                // If any path from neighbor doesn't lead to destination, mark as invalid
53                memo[currentNode] = -1;
54                return false;
55            }
56        }
57      
58        // All paths from this node lead to destination
59        memo[currentNode] = 1;
60        return true;
61    }
62}
63
1class Solution {
2public:
3    bool leadsToDestination(int n, vector<vector<int>>& edges, int source, int destination) {
4        // Track visited nodes during current DFS path to detect cycles
5        vector<bool> visitedInPath(n, false);
6      
7        // Build adjacency list representation of the graph
8        vector<vector<int>> adjacencyList(n);
9      
10        // Memoization array: 0 = unvisited, 1 = valid path, -1 = invalid path
11        vector<int> memo(n, 0);
12      
13        // Construct the adjacency list from edges
14        for (const auto& edge : edges) {
15            adjacencyList[edge[0]].push_back(edge[1]);
16        }
17      
18        // DFS function to check if all paths from current node lead to destination
19        function<bool(int)> dfs = [&](int currentNode) -> bool {
20            // Base case: reached destination node
21            if (currentNode == destination) {
22                // Destination should have no outgoing edges (terminal node)
23                return adjacencyList[currentNode].empty();
24            }
25          
26            // If already computed, return cached result
27            if (memo[currentNode] != 0) {
28                return memo[currentNode] == 1;
29            }
30          
31            // Check for cycle (node visited in current path) or dead-end (non-destination leaf)
32            if (visitedInPath[currentNode] || adjacencyList[currentNode].empty()) {
33                return false;
34            }
35          
36            // Mark current node as visited in this path
37            visitedInPath[currentNode] = true;
38          
39            // Explore all neighboring nodes
40            for (int neighbor : adjacencyList[currentNode]) {
41                if (!dfs(neighbor)) {
42                    // If any path doesn't lead to destination, mark as invalid
43                    memo[currentNode] = -1;
44                    return false;
45                }
46            }
47          
48            // All paths from this node lead to destination
49            memo[currentNode] = 1;
50            return true;
51        };
52      
53        // Start DFS from source node
54        return dfs(source);
55    }
56};
57
1function leadsToDestination(n: number, edges: number[][], source: number, destination: number): boolean {
2    // Track visited nodes during current DFS path to detect cycles
3    const visitedInPath: boolean[] = new Array(n).fill(false);
4  
5    // Build adjacency list representation of the graph
6    const adjacencyList: number[][] = Array.from({ length: n }, () => []);
7  
8    // Memoization array: 0 = unvisited, 1 = valid path, -1 = invalid path
9    const memo: number[] = new Array(n).fill(0);
10  
11    // Construct the adjacency list from edges
12    for (const edge of edges) {
13        adjacencyList[edge[0]].push(edge[1]);
14    }
15  
16    // DFS function to check if all paths from current node lead to destination
17    const dfs = (currentNode: number): boolean => {
18        // Base case: reached destination node
19        if (currentNode === destination) {
20            // Destination should have no outgoing edges (terminal node)
21            return adjacencyList[currentNode].length === 0;
22        }
23      
24        // If already computed, return cached result
25        if (memo[currentNode] !== 0) {
26            return memo[currentNode] === 1;
27        }
28      
29        // Check for cycle (node visited in current path) or dead-end (non-destination leaf)
30        if (visitedInPath[currentNode] || adjacencyList[currentNode].length === 0) {
31            return false;
32        }
33      
34        // Mark current node as visited in this path
35        visitedInPath[currentNode] = true;
36      
37        // Explore all neighboring nodes
38        for (const neighbor of adjacencyList[currentNode]) {
39            if (!dfs(neighbor)) {
40                // If any path doesn't lead to destination, mark as invalid
41                memo[currentNode] = -1;
42                // Unmark current node before returning (backtrack)
43                visitedInPath[currentNode] = false;
44                return false;
45            }
46        }
47      
48        // Unmark current node after exploring all neighbors (backtrack)
49        visitedInPath[currentNode] = false;
50      
51        // All paths from this node lead to destination
52        memo[currentNode] = 1;
53        return true;
54    };
55  
56    // Start DFS from source node
57    return dfs(source);
58}
59

Time and Space Complexity

Time Complexity: O(V + E) where V is the number of vertices (n) and E is the number of edges.

The algorithm performs a depth-first search (DFS) traversal of the graph. Due to the @cache decorator and the vis set working together:

  • Each node is visited at most once during the DFS traversal (the vis set ensures we don't revisit nodes in the current path, preventing infinite loops in cycles)
  • The @cache decorator memoizes results, so if we encounter the same node through different paths, we return the cached result in O(1) time
  • Building the adjacency list takes O(E) time as we iterate through all edges once
  • The DFS explores each edge at most once, contributing O(E) to the traversal
  • Each node is processed at most once, contributing O(V) to the traversal

Space Complexity: O(V + E)

The space usage comes from several components:

  • The adjacency list g stores all edges, requiring O(E) space
  • The vis set can contain at most O(V) nodes in the worst case (when traversing a long path)
  • The @cache decorator stores memoized results for at most O(V) different nodes
  • The recursive call stack depth can be at most O(V) in the worst case (a linear chain of nodes)
  • Overall, the space complexity is O(V + E) dominated by the adjacency list and the combined space of memoization, visited set, and recursion stack

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall: Incorrect Cycle Detection with Memoization

The Problem: The provided solution has a critical bug in how it handles the visited set with memoization. The visited set is used to track nodes in the current DFS path for cycle detection, but it's never cleared after exploring a path. When combined with the @cache decorator, this creates incorrect behavior:

  1. Once a node is added to visited, it remains there permanently
  2. The cached result for a node might be based on an incorrect cycle detection from a previous path
  3. Valid paths might be incorrectly marked as having cycles

Example Scenario: Consider a graph where:

  • Path 1: source β†’ A β†’ B β†’ destination
  • Path 2: source β†’ C β†’ B β†’ destination

When exploring Path 1, node B is added to visited. Later, when exploring Path 2 through node C, if we try to visit B again, it will incorrectly appear as a cycle because B is still in visited from the previous path exploration.

The Solution: Remove nodes from the visited set after exploring them (backtracking), ensuring that visited only contains nodes in the current DFS path:

@cache
def dfs(current_node: int) -> bool:
    # Base case: reached destination
    if current_node == destination:
        return len(graph[current_node]) == 0
  
    # Check for cycle or dead-end
    if current_node in visited or len(graph[current_node]) == 0:
        return False
  
    # Mark current node as visited
    visited.add(current_node)
  
    # Explore all neighbors
    result = True
    for neighbor in graph[current_node]:
        if not dfs(neighbor):
            result = False
            break
  
    # CRITICAL: Remove from visited set after exploration (backtrack)
    visited.remove(current_node)
  
    return result

Alternative Approach - Using Node States: A more robust approach is to use three states for nodes instead of a simple visited set:

def leadsToDestination(self, n: int, edges: List[List[int]], source: int, destination: int) -> bool:
    graph = defaultdict(list)
    for from_node, to_node in edges:
        graph[from_node].append(to_node)
  
    # States: 0 = unvisited, 1 = visiting (in current path), 2 = visited (fully processed)
    state = [0] * n
  
    def dfs(node: int) -> bool:
        if state[node] == 1:  # Cycle detected
            return False
        if state[node] == 2:  # Already processed
            return True
      
        if node == destination:
            return len(graph[node]) == 0
      
        if len(graph[node]) == 0:  # Dead end that's not destination
            return False
      
        state[node] = 1  # Mark as visiting
      
        for neighbor in graph[node]:
            if not dfs(neighbor):
                return False
      
        state[node] = 2  # Mark as visited
        return True
  
    return dfs(source)

This three-state approach clearly distinguishes between:

  • Nodes currently being explored (state 1) - for cycle detection
  • Nodes fully processed (state 2) - for memoization
  • Unvisited nodes (state 0) - yet to be explored

This prevents the confusion between cycle detection and memoization that occurs in the original solution.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Problem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.

Which of the following method should we use to solve this problem?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More