class Solution:
def leadsToDestination(self, n, edges, source, destination):
from collections import defaultdict
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
visited = [0] * n # 0 = unvisited, 1 = visiting, 2 = visited
def dfs(node):
if visited[node] == 1:
return False # found a cycle
if visited[node] == 2:
return True # already checked
if not graph[node]:
return node == destination
visited[node] = 1
for nei in graph[node]:
if not dfs(nei):
return False
visited[node] = 2
return True
return dfs(source)
class Solution {
public:
bool leadsToDestination(int n, vector<vector<int>>& edges, int source, int destination) {
vector<vector<int>> graph(n);
for (auto& edge : edges) {
graph[edge[0]].push_back(edge[1]);
}
vector<int> visited(n, 0); // 0 = unvisited, 1 = visiting, 2 = visited
function<bool(int)> dfs = [&](int node) {
if (visited[node] == 1) return false; // cycle
if (visited[node] == 2) return true; // already checked
if (graph[node].empty()) return node == destination;
visited[node] = 1;
for (int nei : graph[node]) {
if (!dfs(nei)) return false;
}
visited[node] = 2;
return true;
};
return dfs(source);
}
};
class Solution {
public boolean leadsToDestination(int n, int[][] edges, int source, int destination) {
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
for (int[] edge : edges) {
graph.get(edge[0]).add(edge[1]);
}
int[] visited = new int[n]; // 0 = unvisited, 1 = visiting, 2 = visited
return dfs(graph, visited, source, destination);
}
private boolean dfs(List<List<Integer>> graph, int[] visited, int node, int dest) {
if (visited[node] == 1) return false;
if (visited[node] == 2) return true;
if (graph.get(node).isEmpty()) return node == dest;
visited[node] = 1;
for (int nei : graph.get(node)) {
if (!dfs(graph, visited, nei, dest)) return false;
}
visited[node] = 2;
return true;
}
}
var leadsToDestination = function(n, edges, source, destination) {
const graph = Array.from({length: n}, () => []);
for (const [u, v] of edges) {
graph[u].push(v);
}
const visited = new Array(n).fill(0); // 0 = unvisited, 1 = visiting, 2 = visited
function dfs(node) {
if (visited[node] === 1) return false; // cycle
if (visited[node] === 2) return true; // already checked
if (graph[node].length === 0) return node === destination;
visited[node] = 1;
for (const nei of graph[node]) {
if (!dfs(nei)) return false;
}
visited[node] = 2;
return true;
}
return dfs(source);
};
You are given a directed graph with n
nodes, labeled from 0
to n - 1
. The graph is represented by a list of edges, where each edge is a pair [u, v]
indicating a directed edge from node u
to node v
.
You are also given two nodes: source
and destination
. The task is to determine whether every possible path starting from source
eventually leads to destination
, with the following constraints:
destination
(otherwise, the path "gets stuck" and fails).destination
are not valid.destination
.
In summary, you must check that all paths from source
either end up at destination
or do not get stuck or trapped in cycles.
At first glance, the problem seems to ask for a simple path search from source
to destination
. However, the requirement that all possible paths must lead to destination
makes it much more challenging.
A brute-force approach would be to enumerate every possible path from source
and check if each one ends at destination
. But this is impractical for large graphs, especially with cycles, as the number of paths can be exponential or even infinite.
The key realization is that we need to:
destination
.destination
).To solve this problem efficiently, we can use Depth-First Search (DFS) with cycle detection and memoization. Here’s how we can break it down:
0
= unvisited1
= currently visiting (in the call stack)2
= fully visited (all paths from here are already checked)source
.1
, we've found a cycle—return False
.destination
. If not, return False
(dead end).destination
.2
to memoize the result.False
. If all pass, return True
.Using this approach, we avoid redundant work by memoizing nodes we've already checked, and we prevent infinite loops by detecting cycles with the "visiting" state.
Let's consider an example:
n = 4 edges = [[0,1],[0,2],[1,3],[2,3]] source = 0 destination = 3
0
. It has two neighbors: 1
and 2
.1
:
1
has neighbor 3
. 3
has no outgoing edges and is the destination
—valid path.2
:
2
has neighbor 3
. Again, valid path.0
lead to 3
, so the answer is True
.
If, for example, edges = [[0,1],[1,0]]
(a cycle between 0 and 1), and destination = 1
, starting from 0
would result in an infinite loop, so the answer would be False
.
Brute-force approach:
O(N + E)
, where N
is the number of nodes and E
is the number of edges. Each node and edge is traversed at most once, thanks to memoization.O(N + E)
for the adjacency list and visited state array. The recursion stack can be up to O(N)
in the worst case (for a long path).This makes the optimized solution efficient even for large graphs, as we avoid redundant traversals and infinite recursion.
In this problem, we learned to check whether all possible paths from a source node in a directed graph eventually lead to a specific destination, without getting stuck or falling into cycles. The core insight is to use DFS with state tracking to detect cycles and dead ends, while memoizing results to prevent redundant work. This approach is both efficient and robust, handling even complex graphs with cycles or multiple branches.
By understanding the constraints and designing our algorithm to check all possible paths, we ensure correctness and efficiency, making this solution both elegant and practical.