Before attempting this problem, you should be comfortable with:
We need to verify two conditions: every path from the source eventually reaches the destination, and there are no cycles that would create infinite paths. A DFS with cycle detection handles both.
We use a three-color marking scheme. Nodes start unvisited (white). When we begin processing a node, we mark it gray. When all its descendants have been fully explored, we mark it black. If we ever encounter a gray node during traversal, we've found a back edge, meaning there's a cycle.
A leaf node (no outgoing edges) must be the destination. If we find any leaf that isn't the destination, or any cycle, we return false.
states array where each node starts as unvisited (null or 0).gray, we found a cycle; return false.black, it's safe; return true.gray and recursively visit all neighbors.false, return false.black and return true.dfs(source).class Solution:
# We don't use the state WHITE as such anywhere. Instead, the "null" value in the states array below is a substitute for WHITE.
GRAY = 1
BLACK = 2
def leadsToDestination(self, n: int, edges: List[List[int]], source: int, destination: int) -> bool:
graph = self.buildDigraph(n, edges)
return self.leadsToDest(graph, source, destination, [None] * n)
def leadsToDest(self, graph, node, dest, states):
# If the state is GRAY, this is a backward edge and hence, it creates a Loop.
if states[node] != None:
return states[node] == Solution.BLACK
# If this is a leaf node, it should be equal to the destination.
if len(graph[node]) == 0:
return node == dest
# Now, we are processing this node. So we mark it as GRAY.
states[node] = Solution.GRAY
for next_node in graph[node]:
# If we get a `false` from any recursive call on the neighbors, we short circuit and return from there.
if not self.leadsToDest(graph, next_node, dest, states):
return False
# Recursive processing done for the node. We mark it BLACK.
states[node] = Solution.BLACK
return True
def buildDigraph(self, n, edges):
graph = [[] for _ in range(n)]
for edge in edges:
graph[edge[0]].append(edge[1])
return graphTime complexity:
Space complexity:
Where represents the number of vertices in the graph and represents the number of edges in the graph.
From this Stack Overflow answer:
A BFS could be reasonable if the graph is undirected (be my guest at showing an efficient algorithm using BFS that would report the cycles in a directed graph!), where each cross edge defines a cycle (edge going from a node to an already visited node). If the cross edge is
{v1, v2}, and the root (in the BFS tree) that contains those nodes isr, then the cycle isr ~ v1 - v2 ~ r(~ is a path, - a single edge), which can be reported almost as easily as in DFS.The only reason to use a BFS would be if you know your (undirected) graph is going to have long paths and small path cover (in other words, deep and narrow). In that case, BFS would require proportionally less memory for its queue than DFS' stack (both still linear of course).
In all other cases, DFS is clearly the winner.
A basic visited set cannot distinguish between nodes currently being processed (in the current DFS path) and nodes fully processed. This fails to detect cycles properly, as revisiting a completed node is valid but revisiting a node in the current path indicates a cycle.
# Wrong: simple visited set
if node in visited:
return True # Doesn't distinguish cycle from valid revisit
visited.add(node)A leaf node (no outgoing edges) represents the end of a path. If any leaf is not the destination, the answer is false. Forgetting this check allows paths that end at wrong nodes to be accepted.
# Wrong: missing leaf node check
if len(graph[node]) == 0:
return True # Should be: return node == destinationThe destination must be a true endpoint with no outgoing edges. If the destination has outgoing edges, paths can continue beyond it, violating the requirement that all paths terminate at the destination.
# Wrong: not checking destination has no outgoing edges
# If graph[destination] is non-empty, paths continue past destinationIf the source cannot reach the destination at all, or if some paths lead to dead ends that are not the destination, the answer should be false. Failing to explore all paths from source misses these cases.