Adjacency list is the standard graph representation. First step for BFS/DFS problems.
from collections import defaultdict
graph = defaultdict(list)
# or: {i: [] for i in range(n)}
BFS explores level-by-level. Gives shortest path in unweighted graphs.
from collections import deque
q = deque([start])
visited = {start}
while q:
node = q.popleft()
for nb in graph[node]:
if nb not in visited:
visited.add(nb)
q.append(nb)
Iterative DFS avoids recursion limits. Same time complexity, explicit stack.
stack = [start]
visited = {start}
while stack:
node = stack.pop()
for nb in graph[node]:
if nb not in visited:
visited.add(nb)
stack.append(nb)
Recursive DFS is the most natural graph traversal. Watch for stack overflow on deep graphs.
def dfs(node):
visited.add(node)
for nb in graph[node]:
if nb not in visited:
dfs(nb)
Undirected edges add both directions. Foundation for graph construction.
graph[u].append(v)
graph[v].append(u)
Initialize queue with all sources. BFS radiates outward in parallel from all starting points.
q = deque()
for i in range(R):
for j in range(C):
if grid[i][j] == 2: q.append((i, j, 0))
while q:
i, j, t = q.popleft()
for di, dj in ((1,0),(-1,0),(0,1),(0,-1)):
ni, nj = i+di, j+dj
if 0<=ni<R and 0<=nj<C and grid[ni][nj]==1:
grid[ni][nj] = 2
q.append((ni, nj, t+1))
Start DFS from border O's to mark safe cells. Everything else gets captured.
for i in range(R):
for j in range(C):
if (i in (0, R-1) or j in (0, C-1)) and board[i][j] == 'O':
dfs(i, j) # mark border O as safe
BFS-based topo sort processes zero-indegree nodes first. Detects cycles if order is incomplete.
from collections import deque
q = deque(n for n in range(V) if indeg[n] == 0)
order = []
while q:
u = q.popleft()
order.append(u)
for v in adj[u]:
indeg[v] -= 1
if indeg[v] == 0: q.append(v)
Min-heap BFS for weighted graphs. Skip stale entries (d > dist[u]). O((V+E) log V). Non-negative weights only.
import heapq
from collections import defaultdict
def dijkstra(graph, src, n):
dist = [float('inf')] * n
dist[src] = 0
heap = [(0, src)]
while heap:
d, u = heapq.heappop(heap)
if d > dist[u]: continue # stale entry
for v, w in graph[u]:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
heapq.heappush(heap, (dist[v], v))
return dist
Basic Union-Find without optimizations. find walks to root. union links roots. Returns False if already connected.
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
def find(self, x):
while self.parent[x] != x:
x = self.parent[x]
return x
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py: return False
self.parent[px] = py
return True
Path compression: point directly to root. Union by rank: attach smaller tree under larger. Near O(1) amortized.
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # path compression
return self.parent[x]
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py: return False
if self.rank[px] < self.rank[py]:
px, py = py, px
self.parent[py] = px
if self.rank[px] == self.rank[py]:
self.rank[px] += 1
return True
Three colors: white (unvisited), gray (in current path), black (finished). Gray → gray = cycle in directed graph.
def hasCycle(n, edges):
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
# 0=white, 1=gray (in stack), 2=black (done)
color = [0] * n
def dfs(u):
color[u] = 1 # gray: entering
for v in graph[u]:
if color[v] == 1: return True # back edge → cycle
if color[v] == 0 and dfs(v): return True
color[u] = 2 # black: done
return False
return any(color[i] == 0 and dfs(i) for i in range(n))
Track parent to avoid counting the edge we came from. If neighbor is visited and not parent → cycle.
def hasCycleUndirected(n, edges):
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
visited = [False] * n
def dfs(u, parent):
visited[u] = True
for v in graph[u]:
if not visited[v]:
if dfs(v, u): return True
elif v != parent: # visited and not parent → cycle
return True
return False
return any(not visited[i] and dfs(i, -1) for i in range(n))
2-color BFS: assign opposite colors to neighbors. If same color conflict → not bipartite. Check all components.
from collections import deque
def isBipartite(graph):
n = len(graph)
color = [-1] * n
for start in range(n):
if color[start] != -1: continue
q = deque([start])
color[start] = 0
while q:
u = q.popleft()
for v in graph[u]:
if color[v] == -1:
color[v] = 1 - color[u]
q.append(v)
elif color[v] == color[u]:
return False
return True
Flood-fill DFS: mark visited by changing '1' to '0'. Each DFS call covers one island. Count DFS starts.
def numIslands(grid):
m, n = len(grid), len(grid[0])
def dfs(i, j):
if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] != '1':
return
grid[i][j] = '0' # mark visited
dfs(i+1, j); dfs(i-1, j); dfs(i, j+1); dfs(i, j-1)
count = 0
for i in range(m):
for j in range(n):
if grid[i][j] == '1':
dfs(i, j)
count += 1
return count
BFS guarantees shortest path in unweighted grid. Mark visited WHEN ADDING to queue (not when popping). O(m*n).
from collections import deque
def shortestPath(grid, start, end):
m, n = len(grid), len(grid[0])
q = deque([(start[0], start[1], 0)])
visited = {(start[0], start[1])}
while q:
r, c, dist = q.popleft()
if (r, c) == tuple(end):
return dist
for dr, dc in (0,1),(0,-1),(1,0),(-1,0):
nr, nc = r + dr, c + dc
if (0 <= nr < m and 0 <= nc < n
and grid[nr][nc] == 0
and (nr, nc) not in visited):
visited.add((nr, nc))
q.append((nr, nc, dist + 1))
return -1
HashMap maps original → clone. DFS creates clone before recursing neighbors to handle cycles.
def cloneGraph(node):
if not node: return None
cloned = {}
def dfs(n):
if n in cloned: return cloned[n]
copy = Node(n.val)
cloned[n] = copy
for nb in n.neighbors:
copy.neighbors.append(dfs(nb))
return copy
return dfs(node)
Reverse flow: DFS from ocean edges inward (uphill). Intersection of pacific-reachable and atlantic-reachable cells.
def pacificAtlantic(heights):
m, n = len(heights), len(heights[0])
pac, atl = set(), set()
def dfs(r, c, visit, prev_h):
if (r, c) in visit or r < 0 or r >= m or c < 0 or c >= n:
return
if heights[r][c] < prev_h: return
visit.add((r, c))
for dr, dc in (0,1),(0,-1),(1,0),(-1,0):
dfs(r+dr, c+dc, visit, heights[r][c])
for c in range(n):
dfs(0, c, pac, 0) # top edge → pacific
dfs(m-1, c, atl, 0) # bottom edge → atlantic
for r in range(m):
dfs(r, 0, pac, 0) # left edge → pacific
dfs(r, n-1, atl, 0) # right edge → atlantic
return list(pac & atl)
Sort edges by weight. Greedily add edge if it connects two components (Union-Find). Stop at n-1 edges.
def kruskal(n, edges):
# edges = [(weight, u, v), ...]
edges.sort()
uf = UnionFind(n)
mst_weight = 0
mst_edges = 0
for w, u, v in edges:
if uf.union(u, v):
mst_weight += w
mst_edges += 1
if mst_edges == n - 1:
break
return mst_weight if mst_edges == n - 1 else -1
Dijkstra from source k. Answer is max distance (time for signal to reach farthest node). -1 if not all reachable.
import heapq
from collections import defaultdict
def networkDelayTime(times, n, k):
graph = defaultdict(list)
for u, v, w in times:
graph[u].append((v, w))
dist = {k: 0}
heap = [(0, k)]
while heap:
d, u = heapq.heappop(heap)
if d > dist.get(u, float('inf')): continue
for v, w in graph[u]:
nd = d + w
if nd < dist.get(v, float('inf')):
dist[v] = nd
heapq.heappush(heap, (nd, v))
return max(dist.values()) if len(dist) == n else -1
Build ordering graph from adjacent word pairs. Topological sort gives valid alphabet. Detect cycles for invalid input.
def alienOrder(words):
graph = {c: set() for w in words for c in w}
for i in range(len(words) - 1):
w1, w2 = words[i], words[i+1]
if len(w1) > len(w2) and w1[:len(w2)] == w2:
return "" # invalid: prefix comes after
for c1, c2 in zip(w1, w2):
if c1 != c2:
graph[c1].add(c2)
break
# DFS topo sort
color = {} # 1=gray, 2=black
order = []
def dfs(c):
if c in color: return color[c] == 2
color[c] = 1
for nb in graph[c]:
if not dfs(nb): return False
color[c] = 2
order.append(c)
return True
return ''.join(reversed(order)) if all(dfs(c) for c in graph) else ''
BFS with implicit graph: each word connects to words differing by one letter. Try all 26 replacements at each position.
from collections import deque
def ladderLength(beginWord, endWord, wordList):
words = set(wordList)
if endWord not in words: return 0
q = deque([(beginWord, 1)])
visited = {beginWord}
while q:
word, steps = q.popleft()
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
nw = word[:i] + c + word[i+1:]
if nw == endWord: return steps + 1
if nw in words and nw not in visited:
visited.add(nw)
q.append((nw, steps + 1))
return 0
Recursive DFS on a grid with bounds + color check. Same pattern for island counting, connected regions, etc.
def fill(grid, i, j, old, new):
if i < 0 or i >= len(grid): return
if j < 0 or j >= len(grid[0]): return
if grid[i][j] != old: return
grid[i][j] = new
for di, dj in [(1,0),(-1,0),(0,1),(0,-1)]:
fill(grid, i+di, j+dj, old, new)
Loop over all nodes, DFS/BFS from unvisited ones, increment count each time. Works on adjacency list or matrix.
visited = set()
count = 0
for node in range(n):
if node not in visited:
count += 1
stack = [node]
while stack:
cur = stack.pop()
if cur in visited: continue
visited.add(cur)
stack.extend(graph[cur])
Kahn's algorithm: start from zero in-degree nodes, peel layers. If len(order) < n, there's a cycle.
from collections import deque, defaultdict
in_deg = [0] * n
graph = defaultdict(list)
for a, b in edges:
graph[b].append(a)
in_deg[a] += 1
q = deque(i for i in range(n) if in_deg[i] == 0)
order = []
while q:
node = q.popleft()
order.append(node)
for nb in graph[node]:
in_deg[nb] -= 1
if in_deg[nb] == 0:
q.append(nb)