Files
codetutor/backend/data/patterns/dfs.yaml

276 lines
8.5 KiB
YAML

name: DFS (Depth-First Search)
slug: dfs
difficulty_level: 3
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: []