182. DFS Reachability
Problem
Given an adjacency list and valid start and target vertices, return whether a directed path exists from start to target. Undirected graphs work when both directions are present. A zero-edge path makes start==target succeed. Cycles and repeated edges are allowed.
Examples
Example 1
dfs_reachable([[1],[0,2],[3],[]],0,3)
=> TrueA directed route reaches 3 despite the cycle near the start.
Example 2
dfs_reachable([[1],[],[]],0,2)
=> FalseThe isolated target is never discovered.
Example 3
dfs_reachable([[]],0,0)
=> TrueThe zero-edge path is sufficient.
Reveal the worked solution and reasoning
Why this mechanism works
Reachability needs to explore each discovered vertex only once. Mark a vertex when scheduling it on the DFS stack, so several incoming edges cannot create duplicate pending work. A stack determines exploration order but does not imply a shortest route.
State and boundaries
seen contains exactly the discovered vertices, each reachable from start. stack contains discovered vertices awaiting processing.
Protocol
- Mark and push start.
- Pop a vertex and test for target.
- Mark and push each unseen neighbor.
- Return false if the stack empties.
Visualization
0 -> 1 -> 2
^ | |
|____| v
3
seen prevents the 1->0 cycle from rescheduling 0Python implementation
def dfs_reachable(adj, start, target):
stack, seen = [start], {start}
while stack:
u = stack.pop()
if u == target:
return True
for v in adj[u]:
if v not in seen:
seen.add(v)
stack.append(v)
return False
Worked execution
dfs_reachable([[1],[0,2],[3],[]],0,3)
L2 dfs_reachable: stack, seen = [start], {start}
adj=[[1], [0, 2], [3], []]; start=0; target=3; stack=[0]; seen={0}
L4 dfs_reachable: u = stack.pop()
adj=[[1], [0, 2], [3], []]; start=0; target=3; stack=[]; seen={0}; u=0
L7 dfs_reachable: for v in adj[u]:
adj=[[1], [0, 2], [3], []]; start=0; target=3; stack=[]; seen={0}; u=0; v=1
L9 dfs_reachable: seen.add(v)
adj=[[1], [0, 2], [3], []]; start=0; target=3; stack=[]; seen={0, 1}; u=0; v=1
L10 dfs_reachable: stack.append(v)
adj=[[1], [0, 2], [3], []]; start=0; target=3; stack=[1]; seen={0, 1}; u=0; v=1
L4 dfs_reachable: u = stack.pop()
adj=[[1], [0, 2], [3], []]; start=0; target=3; stack=[]; seen={0, 1}; u=1; v=1
L7 dfs_reachable: for v in adj[u]:
adj=[[1], [0, 2], [3], []]; start=0; target=3; stack=[]; seen={0, 1}; u=1; v=0
L7 dfs_reachable: for v in adj[u]:
adj=[[1], [0, 2], [3], []]; start=0; target=3; stack=[]; seen={0, 1}; u=1; v=2
... 3 intermediate state changes omitted ...
L7 dfs_reachable: for v in adj[u]:
adj=[[1], [0, 2], [3], []]; start=0; target=3; stack=[]; seen={0, 1, 2}; u=2; v=3
L9 dfs_reachable: seen.add(v)
adj=[[1], [0, 2], [3], []]; start=0; target=3; stack=[]; seen={0, 1, 2, 3}; u=2; v=3
L10 dfs_reachable: stack.append(v)
adj=[[1], [0, 2], [3], []]; start=0; target=3; stack=[3]; seen={0, 1, 2, 3}; u=2; v=3
L4 dfs_reachable: u = stack.pop()
adj=[[1], [0, 2], [3], []]; start=0; target=3; stack=[]; seen={0, 1, 2, 3}; u=3; v=3Result: True. A directed route reaches 3 despite the cycle near the start.
dfs_reachable([[1],[],[]],0,2)
L2 dfs_reachable: stack, seen = [start], {start}
adj=[[1], [], []]; start=0; target=2; stack=[0]; seen={0}
L4 dfs_reachable: u = stack.pop()
adj=[[1], [], []]; start=0; target=2; stack=[]; seen={0}; u=0
L7 dfs_reachable: for v in adj[u]:
adj=[[1], [], []]; start=0; target=2; stack=[]; seen={0}; u=0; v=1
L9 dfs_reachable: seen.add(v)
adj=[[1], [], []]; start=0; target=2; stack=[]; seen={0, 1}; u=0; v=1
L10 dfs_reachable: stack.append(v)
adj=[[1], [], []]; start=0; target=2; stack=[1]; seen={0, 1}; u=0; v=1
L4 dfs_reachable: u = stack.pop()
adj=[[1], [], []]; start=0; target=2; stack=[]; seen={0, 1}; u=1; v=1Result: False. The isolated target is never discovered.
Why the solution is correct
The start is reachable, and every newly scheduled neighbor extends a known path. Thus a returned target is reachable. If the search exhausts, every edge from every discovered vertex has been explored, so no reachable vertex remains undiscovered. Each vertex is scheduled at most once.
Time and space
O(V+E) worst-case time and O(V) auxiliary space over the reachable portion of the graph.
Common mistakes
Marking only after pop permits duplicate scheduling. A visited value must identify the vertex, not a possibly repeated display label.
Reasoning card
State: seen contains exactly the discovered vertices, each reachable from start. stack contains discovered vertices awaiting processing.
Invariant: Every scheduled node is reachable and processed at most once.
Decision to rehearse: Use a stack/recursion and visited set.
Finish: Return false if the stack empties
Transfer exercise
Store a parent when discovering each vertex and reconstruct one path when the target is reached.