feat(patterns): graph/tree traversal tutorials
This commit is contained in:
275
backend/data/patterns/dfs.yaml
Normal file
275
backend/data/patterns/dfs.yaml
Normal file
@@ -0,0 +1,275 @@
|
||||
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: []
|
||||
Reference in New Issue
Block a user