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)
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)
BFS finds shortest path ONLY when: 1. Unweighted graph: All edges have equal weight (or weight = 1) 2. 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.
# 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
# Pattern: graph_clone# See: docs/patterns/graph/templates.md Section 2classNode:def__init__(self,val=0,neighbors=None):self.val=valself.neighbors=neighborsifneighborselse[]classSolutionDFS:defcloneGraph(self,node:'Node')->'Node':""" Clone graph using DFS with hash map for node mapping. Key Insight: - Map: original node β cloned node - On first visit: create clone and add to map - On subsequent visits: return existing clone from map - This handles cycles: when we see a visited node, we use its clone Why hash map? - Detect already-cloned nodes (handles cycles) - Retrieve clone when building neighbor lists """ifnotnode:returnNone# Map: original node β cloned nodeold_to_new:dict[Node,Node]={}defdfs(original:Node)->Node:# If already cloned, return the cloneiforiginalinold_to_new:returnold_to_new[original]# Create clone (without neighbors yet)clone=Node(original.val)old_to_new[original]=clone# Clone all neighbors recursivelyforneighborinoriginal.neighbors:clone.neighbors.append(dfs(neighbor))returnclonereturndfs(node)
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.
# 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.
# 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
# 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...
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.
# 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
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.
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
# 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
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?
# 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
classSolutionIterativeDFS:defcanVisitAllRooms(self,rooms:List[List[int]])->bool:"""Iterative DFS using explicit stack."""n=len(rooms)visited:set[int]={0}stack:list[int]=[0]whilestack:room=stack.pop()forkeyinrooms[room]:ifkeynotinvisited:visited.add(key)stack.append(key)returnlen(visited)==n
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.
# 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)
fromcollectionsimportdeque,defaultdictclassSolutionBFS:defvalidPath(self,n:int,edges:List[List[int]],source:int,destination:int)->bool:"""BFS approach with early termination."""ifsource==destination:returnTruegraph:dict[int,list[int]]=defaultdict(list)foru,vinedges:graph[u].append(v)graph[v].append(u)visited:set[int]={source}queue:deque[int]=deque([source])whilequeue:node=queue.popleft()forneighboringraph[node]:ifneighbor==destination:returnTrueifneighbornotinvisited:visited.add(neighbor)queue.append(neighbor)returnFalse
classSolutionUnionFind:defvalidPath(self,n:int,edges:List[List[int]],source:int,destination:int)->bool:""" Union-Find approach - check if source and destination are in same component. When to use Union-Find vs DFS/BFS: - Single query: DFS/BFS is simpler - Multiple queries: Union-Find is more efficient (preprocess once) - Dynamic connectivity: Union-Find handles edge additions """parent=list(range(n))rank=[0]*ndeffind(x:int)->int:ifparent[x]!=x:parent[x]=find(parent[x])# Path compressionreturnparent[x]defunion(x:int,y:int)->None:px,py=find(x),find(y)ifpx==py:return# Union by rankifrank[px]<rank[py]:px,py=py,pxparent[py]=pxifrank[px]==rank[py]:rank[px]+=1# Union all edgesforu,vinedges:union(u,v)# Check if same componentreturnfind(source)==find(destination)
# 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
defcount_components(grid:List[List[str]])->int:"""Count connected components in a grid."""ifnotgrid:return0rows,cols=len(grid),len(grid[0])count=0defdfs(row:int,col:int)->None:if(row<0orrow>=rowsorcol<0orcol>=colsorgrid[row][col]!='1'):returngrid[row][col]='0'# Mark visiteddfs(row+1,col)dfs(row-1,col)dfs(row,col+1)dfs(row,col-1)forrinrange(rows):forcinrange(cols):ifgrid[r][c]=='1':count+=1dfs(r,c)returncount
fromcollectionsimportdequedefmulti_source_bfs(grid:List[List[int]],sources:List[tuple[int,int]])->int:"""BFS from multiple sources simultaneously. Returns max distance."""rows,cols=len(grid),len(grid[0])directions=[(0,1),(0,-1),(1,0),(-1,0)]visited=set(sources)queue=deque(sources)distance=0whilequeue:distance+=1for_inrange(len(queue)):# Process level by levelrow,col=queue.popleft()fordr,dcindirections:nr,nc=row+dr,col+dcif(0<=nr<rowsand0<=nc<colsand(nr,nc)notinvisitedandgrid[nr][nc]==1):visited.add((nr,nc))queue.append((nr,nc))returndistance-1# Adjust for initial increment
defclone_graph(node:'Node')->'Node':"""Deep copy a graph using DFS."""ifnotnode:returnNoneold_to_new:dict[Node,Node]={}defdfs(original:'Node')->'Node':iforiginalinold_to_new:returnold_to_new[original]clone=Node(original.val)old_to_new[original]=cloneforneighborinoriginal.neighbors:clone.neighbors.append(dfs(neighbor))returnclonereturndfs(node)
fromcollectionsimportdequedefis_bipartite(graph:List[List[int]])->bool:"""Check if graph is bipartite using BFS coloring."""n=len(graph)color=[-1]*n# -1 = uncoloreddefbfs(start:int)->bool:queue=deque([start])color[start]=0whilequeue:node=queue.popleft()forneighboringraph[node]:ifcolor[neighbor]==-1:color[neighbor]=1-color[node]queue.append(neighbor)elifcolor[neighbor]==color[node]:returnFalsereturnTruefornodeinrange(n):ifcolor[node]==-1:ifnotbfs(node):returnFalsereturnTrue
defcan_reach(graph:dict[int,list[int]],start:int,target:int)->bool:"""Check if target is reachable from start."""visited=set()defdfs(node:int)->bool:ifnode==target:returnTrueifnodeinvisited:returnFalsevisited.add(node)forneighboringraph.get(node,[]):ifdfs(neighbor):returnTruereturnFalsereturndfs(start)
Document generated for NeetCode Practice Framework β API Kernel: GraphDFS