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:
- At least one path exists from
source
todestination
- 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
- No infinite loops: The number of possible paths from
source
todestination
must be finite (meaning there are no cycles that can be reached fromsource
)
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 thandestination
, returnfalse
- If there's a cycle reachable from
source
, returnfalse
(as this creates infinite paths) - If all paths from
source
eventually reachdestination
and nowhere else, returntrue
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:
-
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. -
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
. -
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 returntrue
- 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 graphvis
: 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:
-
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.
-
DFS Function with Memoization:
@cache def dfs(i):
The
dfs
function recursively explores all paths from nodei
. The@cache
decorator ensures we don't recompute results for nodes we've already fully explored. -
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.
-
-
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
-
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 EvaluatorExample 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:
-
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)
-
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)
-
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}
-
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)
-
Reach destination again: dfs(3)
- Due to
@cache
, we don't recompute - immediately returnTrue
- Remove 2 from
vis
(backtrack):vis = {0}
- Due to
-
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 inO(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, requiringO(E)
space - The
vis
set can contain at mostO(V)
nodes in the worst case (when traversing a long path) - The
@cache
decorator stores memoized results for at mostO(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:
- Once a node is added to
visited
, it remains there permanently - The cached result for a node might be based on an incorrect cycle detection from a previous path
- 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.
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
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
Topological Sort Topological Order Prereq Breadth First Search Review problems graph_bfs Directed Graph Before we get started on topological order let's talk about directed graphs first A graph is directed when its edges have directions these edges are also called arcs Suppose that v w are vertices in a directed
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donβt Miss This!