278 lines
8.6 KiB
YAML
278 lines
8.6 KiB
YAML
name: DFS (Depth-First Search)
|
|
slug: dfs
|
|
difficulty_level: 3
|
|
pattern_type: algorithm
|
|
display_order: 7
|
|
|
|
description: >
|
|
Explore as deep as possible along each branch before backtracking, using
|
|
recursion or an explicit stack. DFS is memory-efficient and naturally suited
|
|
for problems involving paths, connectivity, and exhaustive exploration.
|
|
|
|
when_to_use: |
|
|
- Finding any path (not necessarily shortest)
|
|
- Detecting cycles in graphs
|
|
- Topological sorting
|
|
- Connected components
|
|
- Tree traversals (preorder, inorder, postorder)
|
|
- Exhaustive search when you need to explore all possibilities
|
|
|
|
metaphor: |
|
|
Imagine exploring a maze by always taking the first available turn until you
|
|
hit a dead end. Then you backtrack to the last intersection and try the next
|
|
option. You keep going deeper until stuck, then retreat and try alternatives.
|
|
|
|
Another analogy: reading a "choose your own adventure" book. You follow one
|
|
story path all the way to its ending, then go back and explore different
|
|
choices you didn't take.
|
|
|
|
core_concept: |
|
|
DFS uses a **stack** (LIFO)—either the call stack via recursion or an explicit
|
|
stack data structure. This naturally explores depth-first: the most recently
|
|
discovered node is explored first.
|
|
|
|
The key insight is that DFS maintains the current path from root to current
|
|
node on the stack. This makes it perfect for:
|
|
|
|
1. **Path problems**: The current path is always available during recursion
|
|
2. **Cycle detection**: If you encounter a node already on the current path,
|
|
there's a cycle
|
|
3. **Backtracking**: Naturally undo choices when returning from recursion
|
|
|
|
Unlike BFS, DFS doesn't guarantee shortest paths, but it uses O(h) memory
|
|
(height of tree/recursion depth) vs BFS's O(w) (width of level).
|
|
|
|
visualization: |
|
|
**Example: DFS traversal of graph**
|
|
|
|
```
|
|
Graph:
|
|
A --- B --- E
|
|
| |
|
|
C --- D --- F
|
|
|
|
DFS from A (alphabetical order):
|
|
|
|
Visit A → explore neighbors
|
|
Visit B → explore neighbors
|
|
Visit D → explore neighbors
|
|
Visit C (already visited? skip or detect cycle)
|
|
Visit F → no unvisited neighbors, backtrack
|
|
Backtrack to D, backtrack to B
|
|
Visit E → no unvisited neighbors, backtrack
|
|
Backtrack to B, backtrack to A
|
|
Visit C → D already visited, backtrack
|
|
Done
|
|
|
|
Order visited: A → B → D → F → E → C
|
|
```
|
|
|
|
**Recursion call stack visualization:**
|
|
|
|
```
|
|
dfs(A)
|
|
└─ dfs(B)
|
|
└─ dfs(D)
|
|
└─ dfs(F) ← returns
|
|
└─ dfs(C) ← already visited
|
|
└─ dfs(E) ← returns
|
|
└─ dfs(C) ← already visited
|
|
```
|
|
|
|
**Cycle detection with colors:**
|
|
|
|
```
|
|
WHITE (0) = unvisited
|
|
GRAY (1) = in current path (on stack)
|
|
BLACK (2) = fully processed
|
|
|
|
If we visit a GRAY node → cycle detected!
|
|
```
|
|
|
|
code_template: |
|
|
def dfs_recursive(graph: dict, start: str, visited: set = None) -> list:
|
|
"""Basic recursive DFS traversal."""
|
|
if visited is None:
|
|
visited = set()
|
|
|
|
visited.add(start)
|
|
result = [start]
|
|
|
|
for neighbor in graph[start]:
|
|
if neighbor not in visited:
|
|
result.extend(dfs_recursive(graph, neighbor, visited))
|
|
|
|
return result
|
|
|
|
|
|
def dfs_iterative(graph: dict, start: str) -> list:
|
|
"""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)
|
|
|
|
# Add neighbors in reverse for same order as recursive
|
|
for neighbor in reversed(graph[node]):
|
|
if neighbor not in visited:
|
|
stack.append(neighbor)
|
|
|
|
return result
|
|
|
|
|
|
def dfs_path_finding(graph: dict, start: str, end: str) -> list:
|
|
"""Find a path from start to end using DFS."""
|
|
def dfs(node: str, path: list) -> list:
|
|
if node == end:
|
|
return path
|
|
|
|
for neighbor in graph[node]:
|
|
if neighbor not in path: # Avoid cycles
|
|
result = dfs(neighbor, path + [neighbor])
|
|
if result:
|
|
return result
|
|
|
|
return []
|
|
|
|
return dfs(start, [start])
|
|
|
|
|
|
def detect_cycle(graph: dict) -> bool:
|
|
"""Detect cycle in directed graph using DFS."""
|
|
WHITE, GRAY, BLACK = 0, 1, 2
|
|
color = {node: WHITE for node in graph}
|
|
|
|
def dfs(node: str) -> bool:
|
|
color[node] = GRAY # Mark as being processed
|
|
|
|
for neighbor in graph[node]:
|
|
if color[neighbor] == GRAY: # Back edge = cycle
|
|
return True
|
|
if color[neighbor] == WHITE and dfs(neighbor):
|
|
return True
|
|
|
|
color[node] = BLACK # Mark as complete
|
|
return False
|
|
|
|
return any(color[node] == WHITE and dfs(node) for node in graph)
|
|
|
|
|
|
def topological_sort(graph: dict) -> list:
|
|
"""Topological sort using DFS (Kahn's algorithm alternative)."""
|
|
visited = set()
|
|
result = []
|
|
|
|
def dfs(node: str):
|
|
visited.add(node)
|
|
for neighbor in graph.get(node, []):
|
|
if neighbor not in visited:
|
|
dfs(neighbor)
|
|
result.append(node) # Add after all descendants
|
|
|
|
for node in graph:
|
|
if node not in visited:
|
|
dfs(node)
|
|
|
|
return result[::-1] # Reverse for topological order
|
|
|
|
recognition_signals:
|
|
- "find path"
|
|
- "detect cycle"
|
|
- "connected components"
|
|
- "topological sort"
|
|
- "all paths"
|
|
- "explore all"
|
|
- "tree traversal"
|
|
- "preorder"
|
|
- "postorder"
|
|
- "inorder"
|
|
- "number of islands"
|
|
- "flood fill"
|
|
|
|
common_mistakes:
|
|
- title: Stack overflow with deep recursion
|
|
description: |
|
|
Recursive DFS on large graphs or trees can exceed Python's default
|
|
recursion limit (usually 1000), causing a stack overflow.
|
|
fix: |
|
|
Either increase the limit with `sys.setrecursionlimit(10000)` or convert
|
|
to iterative DFS with an explicit stack. Iterative is safer for unknown
|
|
input sizes.
|
|
|
|
- title: Marking visited too late
|
|
description: |
|
|
In iterative DFS, checking visited only when popping (not when pushing)
|
|
allows the same node to be added multiple times, wasting memory.
|
|
fix: |
|
|
For iterative DFS, either check when popping (simpler but less efficient)
|
|
or mark visited when pushing (more efficient):
|
|
```python
|
|
if neighbor not in visited:
|
|
visited.add(neighbor) # Mark when adding
|
|
stack.append(neighbor)
|
|
```
|
|
|
|
- title: Confusing visited vs current path
|
|
description: |
|
|
For cycle detection in directed graphs, using a simple visited set doesn't
|
|
distinguish between "node in current path" (cycle) and "node fully processed"
|
|
(not a cycle, just already explored).
|
|
fix: |
|
|
Use three states (WHITE/GRAY/BLACK) or track the current path separately.
|
|
A cycle exists only if you revisit a node that's still on the current path.
|
|
|
|
- title: Wrong order in topological sort
|
|
description: |
|
|
Adding nodes to result before processing descendants gives reverse
|
|
topological order or incorrect order entirely.
|
|
fix: |
|
|
Add nodes to result only after all descendants are processed (post-order).
|
|
Then reverse the result at the end.
|
|
|
|
variations:
|
|
- name: Tree DFS (preorder/inorder/postorder)
|
|
description: |
|
|
Visit nodes in specific orders relative to processing children. Preorder
|
|
visits parent first, inorder visits between children, postorder visits
|
|
parent last.
|
|
example: "Binary Tree Inorder Traversal, Serialize/Deserialize BST"
|
|
|
|
- name: Graph DFS
|
|
description: |
|
|
Traverse all reachable nodes from a starting point. Need to track visited
|
|
nodes to avoid infinite loops in cyclic graphs.
|
|
example: "Number of Islands, Clone Graph, Course Schedule"
|
|
|
|
- name: DFS with backtracking
|
|
description: |
|
|
Explore paths and undo choices when they don't lead to a solution. The
|
|
natural call stack provides the backtracking mechanism.
|
|
example: "N-Queens, Sudoku Solver, Permutations"
|
|
|
|
- name: DFS with memoization
|
|
description: |
|
|
Cache results of DFS from each node to avoid recomputation. Transforms
|
|
DFS into dynamic programming for certain problems.
|
|
example: "Longest Increasing Path in Matrix, Word Break"
|
|
|
|
- name: Iterative deepening DFS
|
|
description: |
|
|
Run DFS with increasing depth limits. Combines BFS's shortest-path
|
|
guarantee with DFS's memory efficiency.
|
|
example: "Finding shortest path when memory is limited"
|
|
|
|
related_patterns:
|
|
- bfs
|
|
- backtracking
|
|
- tree-traversal
|
|
- matrix-traversal
|
|
|
|
prerequisite_patterns: []
|