63 Init adjacency list 10s

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)}
64 BFS template 15s

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)
65 DFS template (stack) 15s

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)
66 DFS recursive 10s

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)
67 Add edge (undirected) 5s

Undirected edges add both directions. Foundation for graph construction.

graph[u].append(v)
graph[v].append(u)
144 Multi-source BFS (rotting oranges) 15s

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))
145 Surrounded regions border DFS 10s

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
146 Topological sort (Kahn's) 15s

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)
221 BFS vs DFS: when to use which 15s

BFS = shortest path. DFS = exhaustive search. For grids: shortest → BFS, reachability → DFS, regions → either.

BFS vs DFS — DECISION GUIDE
────────────────────────────
USE BFS when:
  □ Shortest path in unweighted graph
  □ Level-by-level processing needed
  □ Closest node satisfying condition
  □ Minimum steps / moves / transforms
  Examples: word ladder, shortest grid path, rotten oranges

USE DFS when:
  □ Exhaustive search / enumerate all paths
  □ Cycle detection, connected components
  □ Topological sort
  □ Tree traversals (inherently DFS)
  □ Backtracking problems
  Examples: number of islands, path exists, clone graph

COMPLEXITY: both O(V + E)

SPACE:
  BFS: O(width) — queue stores entire level
  DFS: O(height/depth) — stack stores path

GRID PROBLEMS:
  "Shortest path"  → BFS (always)
  "Can you reach?"  → DFS (simpler) or BFS
  "Count regions"   → DFS or BFS (both fine)
  "All paths"       → DFS with backtracking
222 Union-Find vs BFS/DFS for components 15s

Union-Find excels at dynamic connectivity queries. BFS/DFS excels at traversal and shortest paths. Different tools for different jobs.

UNION-FIND vs BFS/DFS FOR COMPONENTS
─────────────────────────────────────
BFS/DFS:                        UNION-FIND:
  One-shot traversal              Online / streaming edges
  O(V + E) per query              O(α(n)) ≈ O(1) per operation
  Need adjacency list             Works with edge list directly
  Simpler code                    Better for dynamic connectivity

USE UNION-FIND when:
  □ Edges arrive one at a time
  □ "Are X and Y connected?" queries
  □ Need to merge groups repeatedly
  □ Kruskal's MST algorithm
  □ Count components as edges added
  Examples: redundant connection, accounts merge

USE BFS/DFS when:
  □ Need to traverse/visit nodes in order
  □ Need shortest path
  □ Graph already built as adjacency list
  □ Need to process each component's nodes
  Examples: number of islands, clone graph

HYBRID: use Union-Find to detect components,
        then BFS/DFS within each component
223 Graph representations: adj list vs matrix vs edge list 15s

Adjacency list is the default for interviews. Use matrix for dense graphs or O(1) edge lookup. Edge list for MST.

GRAPH REPRESENTATIONS
─────────────────────
ADJACENCY LIST: graph[u] = [v1, v2, ...]
  + Space: O(V + E) — sparse-friendly
  + Iterate neighbors: O(degree)
  + Add edge: O(1)
  - Check edge exists: O(degree)
  Best for: most problems, sparse graphs

ADJACENCY MATRIX: graph[u][v] = weight
  + Check edge exists: O(1)
  + Dense graph efficient
  - Space: O(V²) — wasteful if sparse
  - Iterate neighbors: O(V)
  Best for: dense graphs, Floyd-Warshall

EDGE LIST: [(u, v, weight), ...]
  + Simple, compact
  + Natural for Kruskal's (sort by weight)
  - Check edge/neighbors: O(E)
  Best for: Kruskal's MST, reading input

PYTHON PATTERNS:
  Adj list: graph = defaultdict(list)
            graph[u].append(v)
  Matrix:   graph = [[0]*n for _ in range(n)]
  Weighted: graph[u].append((v, weight))
224 Cycle detection: directed vs undirected 15s

Directed uses 3 colors (gray = on stack = cycle). Undirected just checks visited + skip parent. Don't mix them up.

CYCLE DETECTION — DIRECTED vs UNDIRECTED
────────────────────────────────────────
UNDIRECTED GRAPH:
  DFS: if neighbor is visited AND not parent → cycle
  Union-Find: if find(u) == find(v) before union → cycle

DIRECTED GRAPH:
  DFS with 3 colors:
    WHITE (0) = unvisited
    GRAY  (1) = in current DFS path (on stack)
    BLACK (2) = fully processed
    If we visit a GRAY node → back edge → CYCLE

  Kahn's BFS (topological sort):
    If not all nodes processed → cycle exists

KEY DIFFERENCE:
  Undirected: any back edge = cycle (skip parent)
  Directed: only GRAY (in-stack) back edge = cycle
            BLACK (cross/forward edge) is OK!

TEMPLATE:
  directed:  color[u] = GRAY → visit → color[u] = BLACK
  undirected: visited[u] = True, pass parent to avoid
              false positive on the edge we came from
225 Dijkstra's algorithm template 15s

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
226 Union-Find: init + find + union 10s

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
227 Union-Find with path compression + rank 15s

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
228 Cycle detection directed (DFS colors) 15s

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))
229 Cycle detection undirected 15s

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))
230 Bipartite check (BFS coloring) 15s

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
231 Number of islands (DFS on grid) 10s

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
232 Shortest path BFS (grid) 15s

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
233 Clone graph (DFS + hashmap) 10s

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)
234 Pacific Atlantic water flow 15s

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)
235 Minimum spanning tree (Kruskal's) 15s

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
236 Network delay time (Dijkstra) 15s

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
237 Alien dictionary (topo sort + DFS) 15s

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 ''
238 Word Ladder BFS 15s

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
263 Flood fill (DFS on grid) 15s

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)
264 Count connected components 15s

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])
265 Topological sort (Kahn's BFS) 20s

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)