You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
API Kernel: GraphDFS, GraphBFSCore Mechanism: Systematically explore all reachable nodes in a graph using depth-first or breadth-first strategies.
This document presents the canonical graph traversal templates covering DFS, BFS, connected components, bipartite checking, and shortest path problems. Each implementation follows consistent naming conventions and includes detailed logic explanations.
defdfs(node: int, graph: dict, visited: set) ->None:
""" DFS template using recursion. Core invariant: - visited set prevents revisiting nodes - Process node BEFORE recursing (preorder) or AFTER (postorder) """ifnodeinvisited:
returnvisited.add(node)
# Process current node (preorder position)forneighboringraph[node]:
dfs(neighbor, graph, visited)
# Process current node (postorder position)
1.4 Universal BFS Template
fromcollectionsimportdequedefbfs(start: int, graph: dict) ->None:
""" BFS template using queue. Core invariant: - Nodes at distance d are processed before nodes at distance d+1 - visited set prevents revisiting and infinite loops """visited: set[int] = {start}
queue: deque[int] =deque([start])
whilequeue:
node=queue.popleft()
# Process current nodeforneighboringraph[node]:
ifneighbornotinvisited:
visited.add(neighbor) # Mark visited BEFORE adding to queuequeue.append(neighbor)
Unweighted graph: All edges have equal weight (or weight = 1)
Non-negative weights: No negative edges
For weighted graphs, use Dijkstra's algorithm instead.
2. Base Template: Number of Islands (LeetCode 200)
Problem: Count the number of islands in a 2D binary grid.
Invariant: Each DFS marks all cells of one island as visited.
Role: BASE TEMPLATE for connected components on grid.
2.1 Pattern Recognition
Signal
Indicator
"count islands/regions"
→ Connected components with DFS/BFS
"2D grid"
→ Grid as implicit graph
"connected 4-directionally"
→ 4-directional neighbors
2.2 Implementation
# Pattern: graph_dfs_connected_components# See: docs/patterns/graph/templates.md Section 1 (Base Template)classSolutionDFS:
defnumIslands(self, grid: List[List[str]]) ->int:
""" Count islands using DFS to mark connected land cells. Key Insight: - Each unvisited '1' starts a new island - DFS from that cell marks all connected '1's as visited - Number of DFS calls = number of islands Why mark in-place? - Change '1' to '0' to mark as visited - Avoids separate visited set (saves space) - Alternatively, use visited set if grid shouldn't be modified """ifnotgridornotgrid[0]:
return0rows, cols=len(grid), len(grid[0])
island_count=0defdfs(row: int, col: int) ->None:
"""Flood fill: mark all connected land cells."""# Boundary check and water/visited checkif (row<0orrow>=rowsorcol<0orcol>=colsorgrid[row][col] !='1'):
return# Mark as visited (sink the land)grid[row][col] ='0'# Explore 4 directionsdfs(row+1, col) # Downdfs(row-1, col) # Updfs(row, col+1) # Rightdfs(row, col-1) # Left# Main loop: find and count islandsforrowinrange(rows):
forcolinrange(cols):
ifgrid[row][col] =='1':
island_count+=1dfs(row, col) # Mark entire island as visitedreturnisland_count
Problem: Find cells that can flow to both Pacific and Atlantic oceans.
Pattern: Multi-source BFS/DFS from ocean borders
Key Insight: Reverse the flow direction - find cells reachable FROM ocean.
4.1 Pattern Recognition
Signal
Indicator
"flow from multiple sources"
→ Multi-source BFS
"reach both destinations"
→ Intersection of two reachability sets
"reverse direction"
→ Start from destination, not source
4.2 Implementation
# Pattern: graph_bfs_multi_source# See: docs/patterns/graph/templates.md Section 3fromcollectionsimportdequeclassSolutionBFS:
defpacificAtlantic(self, heights: List[List[int]]) ->List[List[int]]:
""" Find cells that can reach both oceans using reverse BFS. Key Insight: - Forward: Check if water from cell reaches ocean (complex) - Reverse: Check if ocean can reach cell going UPHILL (simpler) Why reverse direction? - Ocean borders are known sources - Multi-source BFS finds all reachable cells efficiently - Intersection gives cells reaching both oceans """ifnotheightsornotheights[0]:
return []
rows, cols=len(heights), len(heights[0])
directions= [(0, 1), (0, -1), (1, 0), (-1, 0)]
defbfs(sources: list[tuple[int, int]]) ->set[tuple[int, int]]:
"""Multi-source BFS to find all reachable cells."""reachable: set[tuple[int, int]] =set(sources)
queue: deque[tuple[int, int]] =deque(sources)
whilequeue:
row, col=queue.popleft()
fordr, dcindirections:
nr, nc=row+dr, col+dc# Can flow uphill (from ocean's perspective)if (0<=nr<rowsand0<=nc<colsand
(nr, nc) notinreachableandheights[nr][nc] >=heights[row][col]):
reachable.add((nr, nc))
queue.append((nr, nc))
returnreachable# Pacific: top row + left columnpacific_sources= (
[(0, col) forcolinrange(cols)] +
[(row, 0) forrowinrange(rows)]
)
# Atlantic: bottom row + right columnatlantic_sources= (
[(rows-1, col) forcolinrange(cols)] +
[(row, cols-1) forrowinrange(rows)]
)
# Find cells reachable from each oceanpacific_reach=bfs(pacific_sources)
atlantic_reach=bfs(atlantic_sources)
# Return intersectionreturnlist(pacific_reach&atlantic_reach)
Problem: Find minimum time for all oranges to rot (multi-source BFS).
Pattern: Multi-source BFS for simultaneous propagation
Key Insight: All rotten oranges spread simultaneously each minute.
5.1 Pattern Recognition
Signal
Indicator
"minimum time/minutes"
→ BFS level = time
"spread simultaneously"
→ Multi-source BFS
"propagation from multiple sources"
→ Initialize queue with all sources
5.2 Implementation
# Pattern: graph_bfs_multi_source# See: docs/patterns/graph/templates.md Section 4fromcollectionsimportdequeclassSolutionBFS:
deforangesRotting(self, grid: List[List[int]]) ->int:
""" Find minimum minutes for all oranges to rot using multi-source BFS. Key Insight: - All rotten oranges spread at the same time (level = minute) - BFS naturally processes level by level - Initialize queue with ALL rotten oranges (multi-source) - Each BFS level = 1 minute of spreading Why BFS, not DFS? - BFS processes by "wavefront" (distance from sources) - Each wavefront = 1 minute - Time = number of wavefronts = BFS depth """ifnotgridornotgrid[0]:
return-1rows, cols=len(grid), len(grid[0])
directions= [(0, 1), (0, -1), (1, 0), (-1, 0)]
# Initialize: find all rotten oranges and count freshqueue: deque[tuple[int, int]] =deque()
fresh_count=0forrowinrange(rows):
forcolinrange(cols):
ifgrid[row][col] ==2:
queue.append((row, col))
elifgrid[row][col] ==1:
fresh_count+=1# Edge case: no fresh orangesiffresh_count==0:
return0minutes=0# Multi-source BFS: process level by levelwhilequeue:
minutes+=1level_size=len(queue)
for_inrange(level_size):
row, col=queue.popleft()
fordr, dcindirections:
nr, nc=row+dr, col+dc# Check bounds and if fresh orangeif (0<=nr<rowsand0<=nc<colsandgrid[nr][nc] ==1):
# Rot the orangegrid[nr][nc] =2fresh_count-=1queue.append((nr, nc))
# Check if all oranges rottedreturnminutes-1iffresh_count==0else-1
# Case 1: Fresh orange unreachable# 2 1 1# 0 1 1# 1 0 1 ← (2,0) is isolated# Return: -1# Case 2: No fresh oranges# 2 2 2# 0 2 0# Return: 0# Case 3: All fresh oranges adjacent to rotten# 2 1# 1 2# Return: 1
5.5 Why -1 at the End?
# After BFS completes:# - minutes counted from 1 (first spreading)# - But initial state is minute 0# - Need to subtract 1 from final count# Alternative: Start minutes = -1, increment before processingminutes=-1whilequeue:
minutes+=1# process level...
5.6 Complexity Analysis
Aspect
Complexity
Time
O(m × n) - each cell visited once
Space
O(m × n) - worst case all oranges in queue
6. Is Graph Bipartite? (LeetCode 785)
Problem: Determine if a graph can be 2-colored (bipartite).
Pattern: BFS/DFS with node coloring
Key Insight: Alternate colors at each level; conflict = not bipartite.
6.1 Pattern Recognition
Signal
Indicator
"two groups" / "bipartite"
→ Two-coloring problem
"no edges within group"
→ Adjacent nodes must differ
"can be divided"
→ Graph coloring BFS/DFS
6.2 Implementation
# Pattern: graph_bipartite# See: docs/patterns/graph/templates.md Section 5fromcollectionsimportdequeclassSolutionBFS:
defisBipartite(self, graph: List[List[int]]) ->bool:
""" Check if graph is bipartite using BFS coloring. Key Insight: - Bipartite = can assign 2 colors so no adjacent nodes share color - BFS processes level by level - Alternate colors at each level - If we try to color a node that's already colored differently → conflict Why BFS? - Natural level-by-level processing - Each level gets opposite color - Easy to detect conflicts """n=len(graph)
# -1 = uncolored, 0 = color A, 1 = color Bcolor: list[int] = [-1] *ndefbfs(start: int) ->bool:
"""BFS coloring from start node. Returns False if conflict."""queue: deque[int] =deque([start])
color[start] =0# Start with color 0whilequeue:
node=queue.popleft()
forneighboringraph[node]:
ifcolor[neighbor] ==-1:
# Uncolored: assign opposite colorcolor[neighbor] =1-color[node]
queue.append(neighbor)
elifcolor[neighbor] ==color[node]:
# Same color as current node → conflictreturnFalsereturnTrue# Check all components (graph may be disconnected)fornodeinrange(n):
ifcolor[node] ==-1:
ifnotbfs(node):
returnFalsereturnTrue
6.3 Trace Example
Graph: [[1,3], [0,2], [1,3], [0,2]]
Adjacency:
0 --- 1
| |
3 --- 2
BFS from node 0:
1. Color 0 with color A (0)
2. Visit neighbors 1, 3: color with B (1)
3. Visit neighbor of 1 (which is 2): color with A (0)
4. Visit neighbor of 3 (which is 2): already colored A ✓
5. Check neighbor of 2 (which is 3): already colored B ✓
Colors: [A, B, A, B] = [0, 1, 0, 1]
No conflicts → Bipartite!
Non-bipartite example:
0 --- 1
\ /
\ /
2
Coloring attempt:
1. Color 0 with A
2. Color 1, 2 with B
3. Check edge 1-2: both B → CONFLICT!
Not bipartite.
6.4 DFS Alternative
classSolutionDFS:
defisBipartite(self, graph: List[List[int]]) ->bool:
"""DFS approach - recursive coloring."""n=len(graph)
color: list[int] = [-1] *ndefdfs(node: int, c: int) ->bool:
"""Try to color node with color c. Returns False if conflict."""ifcolor[node] !=-1:
returncolor[node] ==c# Check for conflictcolor[node] =cforneighboringraph[node]:
ifnotdfs(neighbor, 1-c):
returnFalsereturnTruefornodeinrange(n):
ifcolor[node] ==-1:
ifnotdfs(node, 0):
returnFalsereturnTrue
6.5 Why Disconnected Graph Check?
# Graph may have multiple components# Example: graph = [[1], [0], [3], [2]]# 0 -- 1 2 -- 3## Must check EACH component separately# Only need to start BFS/DFS from uncolored nodesfornodeinrange(n):
ifcolor[node] ==-1: # New componentifnotbfs(node):
returnFalse
6.6 Complexity Analysis
Approach
Time
Space
BFS
O(V + E)
O(V) for color array + queue
DFS
O(V + E)
O(V) for color array + recursion
6.7 Related Problems
Problem
Connection
LC 886: Possible Bipartition
Same pattern with edge list input
LC 207: Course Schedule
Graph cycle detection variant
7. Keys and Rooms (LeetCode 841)
Problem: Determine if all rooms can be visited starting from room 0.
Pattern: DFS/BFS reachability check
Key Insight: Standard graph traversal - can we reach all nodes from source?
7.1 Pattern Recognition
Signal
Indicator
"can visit all" / "reachability"
→ Graph traversal from source
"keys to unlock"
→ Directed edges (key → room)
"start from room 0"
→ Single-source traversal
7.2 Implementation
# Pattern: graph_dfs_reachability# See: docs/patterns/graph/templates.md Section 1classSolutionDFS:
defcanVisitAllRooms(self, rooms: List[List[int]]) ->bool:
""" Check if all rooms are reachable from room 0 using DFS. Key Insight: - rooms[i] = list of keys in room i - Key to room j = directed edge i → j - Question: Can we reach all nodes from node 0? This is a standard reachability problem: - DFS/BFS from start node - Track visited nodes - Check if all nodes visited """n=len(rooms)
visited: set[int] =set()
defdfs(room: int) ->None:
ifroominvisited:
returnvisited.add(room)
# Each key in current room leads to another roomforkeyinrooms[room]:
dfs(key)
# Start from room 0 (always unlocked)dfs(0)
# Check if all rooms visitedreturnlen(visited) ==n
7.3 Trace Example
Input: rooms = [[1], [2], [3], []]
Room 0 has key to room 1
Room 1 has key to room 2
Room 2 has key to room 3
Room 3 has no keys
Graph representation:
0 → 1 → 2 → 3
DFS from room 0:
1. Visit 0, get key 1
2. Visit 1, get key 2
3. Visit 2, get key 3
4. Visit 3, no more keys
visited = {0, 1, 2, 3}
len(visited) == 4 == n → True
Input: rooms = [[1,3], [3,0,1], [2], [0]]
0 → 1, 3
1 → 3, 0, 1
2 → 2 (self-loop)
3 → 0
DFS from room 0:
1. Visit 0, get keys 1, 3
2. Visit 1, get keys (3, 0, 1 - all visited or will visit)
3. Visit 3, get key 0 (visited)
visited = {0, 1, 3}
Room 2 never visited!
len(visited) == 3 != 4 → False
Where V = number of rooms, E = total number of keys
7.7 Related Problems
Problem
Connection
LC 547: Number of Provinces
Count connected components
LC 1971: Find if Path Exists
Same reachability pattern
8. Find if Path Exists in Graph (LeetCode 1971)
Problem: Determine if a path exists between source and destination.
Pattern: DFS/BFS reachability OR Union-Find
Key Insight: Standard connectivity check - multiple valid approaches.
8.1 Pattern Recognition
Signal
Indicator
"path exists between"
→ Reachability query
"undirected graph"
→ Bidirectional edges
"source to destination"
→ Point-to-point connectivity
8.2 Implementation
# Pattern: graph_dfs_reachability# See: docs/patterns/graph/templates.md Section 1fromcollectionsimportdefaultdictclassSolutionDFS:
defvalidPath(self, n: int, edges: List[List[int]], source: int, destination: int) ->bool:
""" Check if path exists from source to destination using DFS. Key Insight: - Build adjacency list from edge list - DFS from source - Return True if we reach destination Early termination: Stop as soon as destination is found. """ifsource==destination:
returnTrue# Build adjacency listgraph: dict[int, list[int]] =defaultdict(list)
foru, vinedges:
graph[u].append(v)
graph[v].append(u) # Undirectedvisited: set[int] =set()
defdfs(node: int) ->bool:
ifnode==destination:
returnTruevisited.add(node)
forneighboringraph[node]:
ifneighbornotinvisited:
ifdfs(neighbor):
returnTruereturnFalsereturndfs(source)
# DFS: Simple reachability, recursive thinking# - Easy to implement# - Good for single query# - Natural for exploring all paths# BFS: Need shortest path or level information# - Guaranteed shortest path in unweighted graph# - Good for single query# - Level-by-level exploration# Union-Find: Multiple connectivity queries# - Preprocess graph once# - O(α(n)) per query after preprocessing# - Handles edge additions efficiently
8.8 Complexity Analysis
Approach
Time
Space
DFS
O(V + E)
O(V + E) graph + O(V) visited + recursion
BFS
O(V + E)
O(V + E) graph + O(V) visited + queue
Union-Find
O(E × α(n))
O(V) for parent/rank arrays
8.9 Related Problems
Problem
Connection
LC 200: Number of Islands
Count connected components
LC 547: Number of Provinces
Same as Number of Islands
LC 684: Redundant Connection
Union-Find cycle detection
9. Pattern Comparison Matrix
9.1 By Problem Type
Problem Type
Best Pattern
Alternative
When to Switch
Count islands/regions
DFS flood fill
BFS
Large grid (avoid stack overflow)
Clone graph
DFS + hash map
BFS + hash map
Personal preference
Multi-source spread
Multi-source BFS
-
Always BFS for simultaneous spread
Bipartite check
BFS coloring
DFS coloring
Personal preference
Reachability
DFS
BFS, Union-Find
Multiple queries → Union-Find
Shortest path (unweighted)
BFS
-
Always BFS for shortest path
Shortest path (weighted)
Dijkstra
Bellman-Ford
Negative edges → Bellman-Ford
9.2 DFS vs BFS Selection
Factor
Choose DFS
Choose BFS
Goal
Existence, all paths
Shortest path, levels
Memory
O(depth)
O(width)
Implementation
Recursive (simpler)
Iterative (queue)
Early termination
Good
Good
Level tracking
Harder
Natural
Cycle detection
Postorder time
Layer check
9.3 By Data Structure
Input
Pattern
Key Consideration
Adjacency list
Standard DFS/BFS
Most common
Edge list
Build adj list first
O(E) preprocessing
Adjacency matrix
Iterate row for neighbors
O(V²) if dense
Grid
Implicit graph
4/8 directional
9.4 Time/Space Complexity Summary
Pattern
Time
Space
DFS (recursive)
O(V + E)
O(V) visited + O(V) stack
DFS (iterative)
O(V + E)
O(V) visited + O(V) stack
BFS
O(V + E)
O(V) visited + O(V) queue
Multi-source BFS
O(V + E)
O(V) for multiple sources
Union-Find
O(E × α(V))
O(V) for parent array
10. Pattern Decision Guide
10.1 Quick Decision Tree
Graph Problem?
├── Need shortest path?
│ ├── Unweighted → BFS
│ ├── Non-negative weights → Dijkstra
│ └── Negative weights → Bellman-Ford
├── Count components?
│ └── DFS/BFS from each unvisited → Count DFS calls
├── Clone/copy structure?
│ └── DFS/BFS + hash map (old → new)
├── Spread from multiple sources?
│ └── Multi-source BFS (queue all sources)
├── Bipartite/two-coloring?
│ └── BFS/DFS with alternating colors
├── Reachability (can reach X from Y)?
│ ├── Single query → DFS/BFS
│ └── Multiple queries → Union-Find
└── Cycle detection?
├── Undirected → Union-Find or DFS with parent tracking
└── Directed → DFS with coloring (white/gray/black)