Graph DFS
O(V + E) SpaceO(V) Think of exploring a cave system. You pick a tunnel and follow it as far as it goes. When you hit a dead end, you backtrack to the last fork and try a different tunnel. You keep doing this until you’ve mapped every reachable chamber.
When to use it: Detecting cycles, finding connected components, topological sorting, checking bipartiteness, finding strongly connected components, or any problem requiring full graph exploration.
Key insight: DFS naturally tracks a path from the start to the current node. This makes it ideal for path-based problems, cycle detection (back edges), and problems that need to process nodes after all their descendants.
Common trap: Not distinguishing between “currently being explored” and “fully explored” nodes. For cycle detection in directed graphs, you need three states: unvisited, in-progress, and complete.
# Basic graph DFS
function dfs(graph, node, visited):
visited.add(node)
process(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs(graph, neighbor, visited)
# Cycle detection in a directed graph
function hasCycle(graph):
WHITE = 0 # unvisited
GRAY = 1 # in current path (being explored)
BLACK = 2 # fully explored
color = {node: WHITE for node in graph}
function dfsVisit(node):
color[node] = GRAY # start exploring
for neighbor in graph[node]:
if color[neighbor] == GRAY:
return true # back edge = cycle
if color[neighbor] == WHITE:
if dfsVisit(neighbor):
return true
color[node] = BLACK # done exploring
return false
for node in graph:
if color[node] == WHITE:
if dfsVisit(node):
return true
return false def dfs_recursive(graph: dict[int, list[int]], start: int) -> list[int]:
"""Return all nodes reachable from start."""
visited = set()
result = []
def dfs(node: int) -> None:
visited.add(node)
result.append(node)
for neighbor in graph.get(node, []):
if neighbor not in visited:
dfs(neighbor)
dfs(start)
return result
def dfs_iterative(graph: dict[int, list[int]], start: int) -> list[int]:
"""Iterative DFS using explicit stack."""
visited = set()
stack = [start]
result = []
while stack:
node = stack.pop()
if node in visited:
continue
visited.add(node)
result.append(node)
for neighbor in graph.get(node, []):
if neighbor not in visited:
stack.append(neighbor)
return result
def has_cycle_directed(graph: dict[int, list[int]]) -> bool:
"""Detect cycle in a directed graph using 3-color DFS."""
WHITE, GRAY, BLACK = 0, 1, 2
color = {node: WHITE for node in graph}
def dfs(node: int) -> bool:
color[node] = GRAY
for neighbor in graph.get(node, []):
if color.get(neighbor, WHITE) == GRAY:
return True
if color.get(neighbor, WHITE) == WHITE and dfs(neighbor):
return True
color[node] = BLACK
return False
return any(dfs(node) for node in graph if color[node] == WHITE)
def connected_components(graph: dict[int, list[int]]) -> list[list[int]]:
"""Find all connected components in an undirected graph."""
visited = set()
components = []
for node in graph:
if node not in visited:
component = []
stack = [node]
while stack:
n = stack.pop()
if n in visited:
continue
visited.add(n)
component.append(n)
for neighbor in graph.get(n, []):
if neighbor not in visited:
stack.append(neighbor)
components.append(component)
return components Problem: Given a directed graph representing course prerequisites, determine if it is possible to finish all courses (i.e., no circular dependencies).
How would you approach this?
Answer: This is cycle detection in a directed graph using DFS. Use the three-color scheme: if DFS encounters a GRAY (in-progress) node, there's a cycle and courses cannot all be completed. If no back edges are found, the schedule is valid.
Problem: Given an undirected graph, count how many connected components it has.
How would you approach this?
Answer: This is graph DFS for connected components. Start DFS from any unvisited node, marking all reachable nodes as visited (one component). Repeat from the next unvisited node. The number of DFS starts equals the number of components.
Time: O(V + E) where V is the number of vertices and E is the number of edges. Each vertex is visited once, and each edge is examined once (directed) or twice (undirected).
Space: O(V) for the visited set and the recursion stack (or explicit stack). In the worst case, the recursion depth equals V (a graph shaped like a long chain).