13 · Graphs: BFS and DFS on grids and adjacency lists Code
Grids as graphs, visited sets, flood fill, connected components · BFS for fewest hops · topological sort and Dijkstra as optional depth
Why it matters for NPE. Grid DFS/BFS is what Minesweeper and island-style questions need, and a Meta employee lists 'simple bfs/dfs/tree/array problems' as in scope. Shortest-path algorithms are rarely asked; know them conceptually because networks are graphs.
Primer: a graph is "things plus who touches whom", and you only need two walks
A graph is a set of nodes and a set of edges between them. Switches and links. Courses and prerequisites. Grid cells and their neighbours. Every problem on this page is one of two walks over that structure: DFS (go as deep as you can, then back up) and BFS (visit everything one hop away, then two hops, then three). DFS answers "what is connected to this" and "how big is this blob". BFS answers the same questions and also "fewest hops". Minesweeper, named in two firsthand NPE reports, is a DFS or BFS on a grid with a stopping rule. Number of Islands is the same walk with a counter. A Meta employee describes the coding bar as "simple bfs/dfs/tree/array problems", so these two walks are the ceiling you are preparing for, not the floor.
Grid indexing, the in_bounds helper, DIRS4 / DIRS8 and the [[0] * c] * r aliasing trap are in topic 11. This page assumes them and starts where topic 11 handed off: "when ships can touch, count connected components".
Watch
3:54 edge list, 5:10 matrix, 6:39 adjacency list, 7:49 recursive DFS, 11:32 iterative DFS with a stack, 14:18 BFS with a queue, 17:27 complexity. 22:20 onward is live Python: type along.
0:00 the idea, 5:00 code. The component-counting template that Max Area, Provinces and the Battleships follow-up all reuse. Attempt it first.
2:05 drawing, 6:05 code. Multi-source BFS by levels: seed the queue with every rotten cell, one level per minute.
1:36 why you never build an adjacency list for a grid, 3:55 direction vectors, 7:51 BFS on the grid step by step. Pseudocode at 11:07; the Python is below.
Best explained, but JavaScript, so translate. Use the chapters: 7:10-29:13 traversal, 1:00:44-1:39:36 components and shortest path, 1:39:36 island count. Skip the rest.
Optional depth. 4:37 drawing, 14:37 heapq code. The only weighted-graph video you need.
Optional depth. 5:36 the in-degree intuition, 6:05 two worked examples, 11:15 pseudocode. Course Schedule I and II are this.
Representing a graph
| Form | Python | Memory | "Is u-v an edge?" | "Neighbours of u" | Use when |
|---|---|---|---|---|---|
| Adjacency list | dict[node, list[node]] | O(V + E) | O(deg u) | O(deg u) | default; sparse graphs, anything you BFS/DFS |
| Adjacency matrix | m[u][v] 0/1 or weight | O(V²) | O(1) | O(V) | dense, small V; the input in Number of Provinces |
| Edge list | [(u, v), …] | O(E) | O(E) | O(E) | only as input; convert it first |
| Implicit grid | grid[r][c] plus a delta list | O(rc), already given | O(1) | O(4) or O(8) | any matrix question; never build a list for it |
from collections import defaultdict, deque
def build(edges, directed=False):
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
if not directed:
graph[v].append(u) # undirected = store the edge both ways
return graph
# nodes with no edges never appear as keys. Iterate the node list, not the dict:
for u in nodes:
for v in graph.get(u, ()): # .get, not graph[u]: a defaultdict read inserts a key
...
# a grid is a graph you never materialise: node = (r, c), neighbours = cells that pass the bounds check
rows, cols = len(grid), len(grid[0])
DIRS4 = [(-1, 0), (1, 0), (0, -1), (0, 1)]
DIRS8 = [(dr, dc) for dr in (-1, 0, 1) for dc in (-1, 0, 1) if (dr, dc) != (0, 0)]
def neighbours(r, c, dirs=DIRS4):
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols:
yield nr, nc
Directed vs undirected. An undirected edge is two directed edges, so build appends both ways. A grid is undirected (if I can step to you, you can step to me). Prerequisites, "A can reach B through a one-way link", and "u calls v" are directed. Direction changes three things: you add each edge once instead of twice, in-degree becomes meaningful (topological sort needs it), and cycle detection changes (in an undirected graph "I see my parent again" is not a cycle; in a directed graph a back edge to any node on the current path is).
Weights ride along as graph[u].append((v, w)). Nothing on this page needs them until the optional Dijkstra section.
Visited: a set, or marking the grid
| Method | Code | Cost | When |
|---|---|---|---|
| Set of nodes | seen = set(); seen.add((r, c)) | O(V) extra, hashing a tuple per check | default; works for any node type; does not touch the input |
| Boolean grid | seen = [[False] * cols for _ in range(rows)] | O(rc) extra, no hashing | large grids where the tuple-set is measurably slow |
| Mark the input | grid[r][c] = '0' after visiting a '1' | O(1) extra | only after asking "may I modify the input?" The caller may need it again. Say you would copy it otherwise. |
| Distance dict | dist = {start: 0} | O(V) | BFS when you need hop counts anyway; the dict is the visited set |
Minesweeper has a fourth option built in: the board itself records state. Writing 'B' or a digit into a cell is what marks it visited, and the problem asks you to mutate the board, so no extra structure is needed.
DFS: recursive, iterative, and the recursion limit
def dfs(graph, u, seen): # recursive: shortest to write, cleanest to read
seen.add(u)
for v in graph[u]:
if v not in seen:
dfs(graph, v, seen)
def dfs_iter(graph, start): # iterative: same reachability, no recursion limit
seen = {start}
stack = [start]
while stack:
u = stack.pop()
for v in graph[u]:
if v not in seen:
seen.add(v) # mark on push
stack.append(v)
return seen
Python's default recursion limit is about 1000 frames. Recursive DFS goes one frame deeper per step of the path, so a snake-shaped island of 5000 cells on a 1000×1000 grid raises RecursionError. Three answers, in the order you should give them: (1) switch to the iterative version with an explicit stack, which is a two-line change; (2) sys.setrecursionlimit(10**6), which lifts the Python limit but can still crash the interpreter on the C stack, so it is a hack and you should call it one; (3) BFS, which never recurses. In a 45-minute screen write the recursive version if it is shorter, then say out loud that it has a depth limit and how you would fix it. That is the fluency point for free.
Cost of either version: O(V + E) time, every node once and every edge once (twice if undirected), and O(V) space for the visited set plus the stack. On a grid V = rc and E ≤ 4rc, so say "O(rc)". The iterative version visits in a slightly different order from the recursive one and may hold a node on the stack more than once if you mark on pop instead of on push. Mark on push.
BFS: a deque, and why it gives the fewest hops
from collections import deque
def bfs(graph, start):
dist = {start: 0} # doubles as the visited set
q = deque([start])
while q:
u = q.popleft()
for v in graph[u]:
if v not in dist: # mark when you ENQUEUE, never when you dequeue
dist[v] = dist[u] + 1
q.append(v)
return dist # every reachable node and its hop count
The queue holds nodes in non-decreasing distance order. Everything at distance d is dequeued before anything at distance d + 1, so the first time you reach a node you reached it by a shortest path. That is the whole proof, and the interviewer wants you to say it, not to cite it. It holds only when every edge costs the same. The moment edges have different weights, see the Dijkstra section.
Two shapes you need from this one template. Distance per node: the dict above. Level by level (when you need "how many rounds" rather than "how far is each node"): drain the queue one layer at a time.
level = 0
while q:
for _ in range(len(q)): # exactly the nodes that were in the queue at the start of this level
u = q.popleft()
...
level += 1
Marking on dequeue instead of enqueue is the single most expensive mistake on this page. A node with k already-queued parents is pushed k times, each push fans out again, and on a dense grid that is exponential duplicate work and a queue that never drains in time. Mark on enqueue.
Connected components: Number of Islands, Max Area, Provinces
A connected component is a maximal set of nodes that can all reach each other. Counting them is: scan every node, and when you hit one you have not seen, count one component and flood from it so you never count its members again. The flood is DFS or BFS, your choice.
Number of Islands (LC 200), worked in full
def num_islands(grid):
rows, cols = len(grid), len(grid[0])
seen = set()
def flood(r, c): # iterative DFS; marks every land cell connected to (r, c)
stack = [(r, c)]
seen.add((r, c))
while stack:
r, c = stack.pop()
for dr, dc in DIRS4:
nr, nc = r + dr, c + dc
if (0 <= nr < rows and 0 <= nc < cols
and grid[nr][nc] == '1' and (nr, nc) not in seen):
seen.add((nr, nc))
stack.append((nr, nc))
islands = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1' and (r, c) not in seen:
islands += 1
flood(r, c)
return islands
O(rc) time: the outer loop touches every cell once, and the floods together touch every land cell once more. O(rc) space for seen and the stack in the worst case (an all-land grid). If you may overwrite, replace seen with grid[nr][nc] = '0' and the extra space drops to the stack. Note the cells are strings '1' here, as on LeetCode; say that you checked.
Follow-ups you should pre-empt: "diagonals count" (swap DIRS4 for DIRS8, one word), "return the sizes" (have flood count and return cells, then sizes.append(...)), "largest island" is Max Area below, "label each cell with its island id" is the Battleships bomb pattern from topic 11.
Max Area of Island (LC 695)
def max_area(grid):
rows, cols = len(grid), len(grid[0])
seen = set()
best = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == 1 and (r, c) not in seen:
area = 0
stack = [(r, c)]; seen.add((r, c))
while stack:
cr, cc = stack.pop()
area += 1
for dr, dc in DIRS4:
nr, nc = cr + dr, cc + dc
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1 and (nr, nc) not in seen:
seen.add((nr, nc)); stack.append((nr, nc))
best = max(best, area)
return best
Same walk, the flood returns a size instead of nothing. Still O(rc).
Number of Provinces (LC 547): components from an adjacency matrix
def find_circle_num(is_connected):
n = len(is_connected)
seen = [False] * n
provinces = 0
for s in range(n):
if seen[s]:
continue
provinces += 1
stack = [s]; seen[s] = True
while stack:
u = stack.pop()
for v in range(n): # neighbours of u are the 1s in row u
if is_connected[u][v] and not seen[v]:
seen[v] = True
stack.append(v)
return provinces
O(n²) because reading a row of the matrix is O(n) and every row is read once. The interviewer may ask for union-find instead; it is below under Accounts Merge, and it is the right answer when edges arrive one at a time and you are asked "how many groups now?" after each.
Flood Fill (LC 733): the one-trap easy
def flood_fill(image, sr, sc, color):
old = image[sr][sc]
if old == color:
return image # without this, every repainted cell still equals old: infinite loop
rows, cols = len(image), len(image[0])
stack = [(sr, sc)]
image[sr][sc] = color # the repaint IS the visited mark
while stack:
r, c = stack.pop()
for dr, dc in DIRS4:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and image[nr][nc] == old:
image[nr][nc] = color
stack.append((nr, nc))
return image
Flood Fill mutates by definition, so the painted value is the visited mark and no set is needed. The trap is the old == color guard: with it missing, a cell painted color still matches old and gets pushed forever. Dry-run that case out loud before the interviewer asks.
Worked in full: Minesweeper (LC 529)
Named in two firsthand NPE reports. The board holds 'M' (hidden mine), 'E' (hidden empty), 'B' (revealed blank), '1'-'8' (revealed, that many mines among the 8 neighbours) and 'X' (a revealed mine). You are given one click. Return the board after the reveal.
The rules, said out loud before coding.
- Click a mine: it becomes
'X'. Done. - Click an
'E': count the mines among its 8 neighbours. If the count is nonzero, write the digit and stop. A number is a wall. - If the count is zero, write
'B'and reveal every'E'neighbour by the same rules. Only zero cells expand. - Clicking an already revealed cell changes nothing. Mines you did not click stay
'M'.
Walk it: (3,0) has zero mines around it, so it becomes 'B' and reveals (2,0), (2,1), (3,1). (2,1) touches the mine at (1,2) diagonally, so it becomes '1' and stops. (2,0) and (3,1) are zeros, so they keep going: up the left column to (0,0), along the bottom row to (3,3). (3,3) reveals (2,2) and (2,3), both '1'. The right-hand 'E's are sealed off by the ring of digits, which is exactly the behaviour you expect from the game.
DFS version (recursive)
DIRS8 = [(dr, dc) for dr in (-1, 0, 1) for dc in (-1, 0, 1) if (dr, dc) != (0, 0)]
def update_board(board, click):
rows, cols = len(board), len(board[0])
r, c = click
if board[r][c] == 'M':
board[r][c] = 'X'
return board
def mines_around(r, c):
n = 0
for dr, dc in DIRS8:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc] == 'M':
n += 1
return n
def reveal(r, c): # precondition: board[r][c] == 'E'
n = mines_around(r, c)
if n:
board[r][c] = str(n)
return # a number is a wall: do not expand past it
board[r][c] = 'B' # mark BEFORE recursing, so neighbours see it as revealed
for dr, dc in DIRS8:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc] == 'E':
reveal(nr, nc)
if board[r][c] == 'E':
reveal(r, c)
return board
The board is the visited set: once a cell is 'B' or a digit it is no longer 'E', so the == 'E' check stops revisits. The recursion depth is the length of the longest chain of zero cells, which on a 50×50 LeetCode board is fine. Say that on a 1000×1000 board you would switch to the BFS below.
BFS version (iterative, no depth limit)
def update_board_bfs(board, click):
rows, cols = len(board), len(board[0])
r, c = click
if board[r][c] == 'M':
board[r][c] = 'X'
return board
if board[r][c] != 'E':
return board # already revealed: nothing to do
def mines_around(r, c):
return sum(1 for dr, dc in DIRS8
if 0 <= r + dr < rows and 0 <= c + dc < cols and board[r + dr][c + dc] == 'M')
def open_cell(r, c): # reveal one 'E'; return True if it is a zero that should expand
n = mines_around(r, c)
board[r][c] = str(n) if n else 'B'
return n == 0
q = deque()
if open_cell(r, c):
q.append((r, c))
while q:
r, c = q.popleft()
for dr, dc in DIRS8:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc] == 'E':
if open_cell(nr, nc): # revealing it is the visited mark; only zeros join the queue
q.append((nr, nc))
return board
Complexity: each cell is revealed at most once and each reveal looks at 8 neighbours, so O(rc) time; the queue or stack holds O(rc) in the worst case. Follow-ups to expect: "what if the click is out of bounds" (validate and return the board), "count how many cells the click revealed" (a counter inside open_cell), "did the player win" (no 'E' left on the board), "do not mutate the input" (copy with [row[:] for row in board] first), "why 8 neighbours and not 4" (the game's rule; say you asked). The recruiter-side comment that this is "easy type" means they expect it clean in under 15 minutes.
Multi-source BFS: Rotting Oranges (LC 994)
Grid of 0 (empty), 1 (fresh), 2 (rotten). Every minute each rotten orange rots its 4 fresh neighbours. Return the minutes until no fresh orange remains, or -1. The trick: seed the queue with every rotten orange at once, then count levels. One BFS from a "virtual source" connected to all of them, instead of one BFS per source.
def oranges_rotting(grid):
rows, cols = len(grid), len(grid[0])
q = deque()
fresh = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == 2:
q.append((r, c)) # every source starts at level 0
elif grid[r][c] == 1:
fresh += 1
minutes = 0
while q and fresh:
for _ in range(len(q)): # one level = one minute
r, c = q.popleft()
for dr, dc in DIRS4:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
grid[nr][nc] = 2 # mark on enqueue; the grid state is the visited set
fresh -= 1
q.append((nr, nc))
minutes += 1
return 0 if fresh == 0 and minutes == 0 else (minutes if fresh == 0 else -1)
O(rc) time and space. The fresh counter is what gives you -1 without a second scan, and the while q and fresh condition is what stops you from counting an extra minute after the last orange rots. Dry-run the all-rotten grid (answer 0) and a fresh orange walled off by zeros (answer -1). Network framing: this is failure propagation across a fabric, or a broadcast storm spreading one hop per tick from several sources at once. "How many ticks until every device has the update" is this exact code.
Shortest Path in Binary Matrix (LC 1091): 8-direction BFS with a distance
n×n grid of 0 (open) and 1 (blocked). Move in 8 directions through open cells from the top-left to the bottom-right. Return the number of cells on the shortest path, or -1. BFS with the distance carried in the queue, and 8 directions because the problem says so. Read that sentence of the prompt twice.
def shortest_path_binary_matrix(grid):
n = len(grid)
if grid[0][0] or grid[n - 1][n - 1]:
return -1
seen = {(0, 0)} # or write 1 into the grid, if you are allowed to
q = deque([(0, 0, 1)]) # (r, c, cells on the path so far); start cell counts as 1
while q:
r, c, d = q.popleft()
if r == n - 1 and c == n - 1:
return d # first time we dequeue the target is the shortest path
for dr, dc in DIRS8:
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and grid[nr][nc] == 0 and (nr, nc) not in seen:
seen.add((nr, nc))
q.append((nr, nc, d + 1))
return -1
O(n²) time and space. Check the target on dequeue, not only when enqueuing neighbours, or the 1×1 grid [[0]] (answer 1) falls through to -1. The same template with DIRS4 and a (r, c) target is "fewest hops from the core switch to this rack".
Clone Graph (LC 133): a hashmap from old node to new node
def clone_graph(node):
if not node:
return None
copies = {node: Node(node.val)} # old → new; doubles as the visited set
q = deque([node])
while q:
u = q.popleft()
for v in u.neighbors:
if v not in copies:
copies[v] = Node(v.val)
q.append(v)
copies[u].neighbors.append(copies[v]) # wire the copy of u to the copy of v
return copies[node]
The dict answers two questions at once: "have I been here" and "where is the copy". Without it, a cycle loops forever and shared neighbours get duplicated. O(V + E). The recursive version is the same with copies as a closure; say the depth caveat.
Accounts Merge (LC 721), and union-find in brief
Each account is [name, email, email, …]. Two accounts are the same person if they share an email. Output merged accounts with sorted emails. This is "connected components where the nodes are emails", plus parsing and sorting. BFS over an email graph works; union-find is shorter and is the structure to name when edges arrive incrementally.
def accounts_merge(accounts):
parent = {}
owner = {} # email → name
def find(x):
parent.setdefault(x, x)
while parent[x] != x:
parent[x] = parent[parent[x]] # path halving: keeps trees flat
x = parent[x]
return x
def union(a, b):
parent[find(a)] = find(b)
for name, *emails in accounts:
for e in emails:
owner[e] = name
union(e, emails[0]) # every email in the account joins the first one
groups = defaultdict(list)
for e in owner:
groups[find(e)].append(e)
return [[owner[root]] + sorted(es) for root, es in groups.items()]
Near-linear: each find is amortised almost O(1) with path compression, plus the sort per group. Union-find answers "same component?" and "how many components?" in O(α(n)) per operation after each new edge; BFS would have to re-walk. Number of Provinces with union-find is the same five lines over the matrix.
Optional depth: topological sort and Dijkstra, with the network framing
Rarely asked at this screen. Know them well enough to explain in two minutes and code in ten, because networks are weighted directed graphs and the interviewer who hears "I would BFS the routing table" will want to know why that is wrong.
Topological sort: Course Schedule (LC 207, 210) with Kahn's algorithm
A topological order of a directed acyclic graph lists every node after all of its prerequisites. Kahn's algorithm: compute in-degrees, queue every node with in-degree 0, pop one, append it to the order, decrement its children, enqueue any child that hits 0. If the order is shorter than V, a cycle exists.
def find_order(num_courses, prerequisites):
graph = defaultdict(list)
indeg = [0] * num_courses
for course, pre in prerequisites: # edge pre → course
graph[pre].append(course)
indeg[course] += 1
q = deque(i for i in range(num_courses) if indeg[i] == 0)
order = []
while q:
u = q.popleft()
order.append(u)
for v in graph[u]:
indeg[v] -= 1
if indeg[v] == 0:
q.append(v)
return order if len(order) == num_courses else [] # LC 207: return len(order) == num_courses
O(V + E). It is BFS with a counter instead of a visited set. Network use: pushing config changes in dependency order (VLAN before the SVI before the routing adjacency), or ordering service restarts. The DFS alternative uses three colours (unvisited, on the current path, done) and reports a cycle when it meets a grey node.
Dijkstra: Network Delay Time (LC 743) with heapq
Directed edges (u, v, w) with w = latency. Signal starts at node k. Return the time until every node has heard it, or -1. Hop count and cost are different things. BFS minimises hops; Dijkstra minimises the sum of weights. In the diagram, BFS says A reaches B in one hop, cost 10. Dijkstra says go through C, two hops, cost 2. Any network with a slow direct link and a fast two-hop path breaks BFS.
import heapq
def network_delay_time(times, n, k):
graph = defaultdict(list)
for u, v, w in times:
graph[u].append((v, w))
dist = {} # node → settled shortest cost
heap = [(0, k)] # (cost so far, node); cost FIRST so the heap orders by it
while heap:
d, u = heapq.heappop(heap)
if u in dist:
continue # stale entry: u was settled earlier with a smaller cost
dist[u] = d
for v, w in graph[u]:
if v not in dist:
heapq.heappush(heap, (d + w, v))
return max(dist.values()) if len(dist) == n else -1
O(E log E) with this lazy-deletion style, which is the one to write under time pressure: push duplicates freely and skip them on pop. Dijkstra requires non-negative weights; a negative edge could make an already-settled node cheaper, and you would need Bellman-Ford. Replace the heap with a deque and you have BFS; that is the clearest way to say the relationship. If all weights are equal, Dijkstra and BFS give the same answer and BFS is faster. If weights are only 0 and 1, a deque with appendleft for the 0 edges works ("0-1 BFS"). Prim's algorithm for Min Cost to Connect All Points is Dijkstra with "edge weight" instead of "cost so far" in the heap tuple.
How real routing relates
OSPF is this section running on every router. The link-state database is an adjacency list with costs, every router runs Dijkstra rooted at itself over it, and the resulting shortest-path tree is what fills the routing table. Hop count is RIP's metric, which is why RIP picks a slow direct link over a fast two-hop path and why nobody runs it at scale. BGP is deliberately not a shortest-path algorithm: it ranks paths by policy attributes first and AS-path length only later. Details, timers and the LSDB are in topic 12; the BGP path selection order is in topic 14. When the interviewer asks "what does OSPF compute", the answer is "Dijkstra over the LSDB, one run per router, re-run when an LSA changes", and the code above is what it looks like.
Interview questions
1. BFS or DFS: when do you pick which?
Both visit every reachable node in O(V + E). BFS when the question is about fewest hops, levels, or "how many rounds", because it discovers nodes in distance order. DFS when the question is about connectivity, component size, paths with backtracking or cycle detection, because it is shorter to write. On a grid I default to iterative DFS for components and BFS for distances.2. Why mark a node visited when you enqueue it rather than when you dequeue it?
Between enqueue and dequeue, other nodes can see it as unvisited and enqueue it again. Each duplicate fans out, so on a dense grid the queue grows far beyond V and the work is no longer O(V + E). Marking on enqueue guarantees each node enters the queue exactly once.3. Complexity of BFS and DFS on an adjacency list, and on a grid?
O(V + E) time and O(V) space for the visited set and the queue or stack. On a grid V is rc and each cell has at most 4 or 8 neighbours, so E is O(rc) and the whole walk is O(rc). Say "O(cells)" rather than "O(n²)" unless the grid is square and n is its side.4. Your recursive DFS is given a 1000×1000 grid that is one long snake. What happens?
Python's recursion limit is about 1000 frames and the snake is up to a million cells deep, so RecursionError. Fix: rewrite with an explicit stack, which is the same code withstack.pop() in a loop. sys.setrecursionlimit is a hack that can still segfault the interpreter. BFS also avoids the problem.5. Number of Islands: what changes if diagonal cells count as connected? If you may not modify the grid?
Diagonals: swap the 4-direction delta list for the 8-direction one, nothing else. No mutation: use aseen set of (r, c) tuples or a boolean grid instead of overwriting cells, at O(rc) extra space. Always ask both questions before coding.6. Minesweeper: walk me through the reveal rules and why a numbered cell does not expand.
Clicking a mine reveals it as X and stops. Clicking an empty cell counts mines in the 8 neighbours; a nonzero count becomes that digit and stops because the player now has information and the cells beyond could be mines. A zero count becomes B and recursively reveals its hidden neighbours, since none of them can be a mine. The board itself is the visited set because revealed cells are no longer E.7. Rotting Oranges: why start BFS from every rotten orange at once, and how do you count minutes?
All rotten oranges spread at the same time, so they are all at level 0 of the same BFS; one BFS per source would recompute overlapping regions and give wrong times. Count minutes by processing the queue one level at a time:for _ in range(len(q)) drains exactly the current level. Track the fresh count to return -1 without rescanning.8. Why can't you use BFS to find the lowest-latency path in a network?
BFS orders nodes by hop count and assumes every edge costs the same. With latencies, a one-hop 10 ms link loses to a two-hop 2 ms path, and BFS would settle the wrong one first. Dijkstra replaces the queue with a min-heap keyed by cost so far, so the cheapest unsettled node is always popped next. With equal weights the two give the same answer.9. Does Dijkstra work with negative edge weights?
No. It settles a node the first time it pops, assuming no later path can be cheaper, and a negative edge breaks that assumption. Bellman-Ford handles negative edges in O(VE) and detects negative cycles. Routing metrics are non-negative, so Dijkstra is what OSPF and IS-IS run.10. How do you detect a cycle in a directed graph? In an undirected one?
Directed: Kahn's algorithm, and a cycle exists if the topological order has fewer than V nodes; or DFS with three colours, where reaching a grey node on the current path is a back edge. Undirected: DFS that treats seeing any visited node other than the parent as a cycle, or union-find where an edge joining two nodes already in the same set closes a cycle.11. Clone Graph: how do you avoid infinite loops and duplicated nodes?
A dict from original node to its copy. Before creating a copy, check the dict; if the node is there, reuse the copy. The dict is both the visited set and the lookup for wiring neighbours, so every original node gets exactly one copy and cycles terminate. O(V + E).12. Given links between switches, which switches become unreachable if switch X fails?
Build an undirected adjacency list, drop X and its edges (or just skip X in the visited check), BFS from the core switch, and return every switch not in the visited set. O(V + E) per failure. "Which single failure strands the most switches" is that loop over every X, O(V · (V + E)); the linear-time answer is Tarjan's articulation points, which I would name and not code in a screen.Traps
- Marking visited on dequeue. Nodes enter the queue many times, the walk stops being O(V + E), and on a dense grid it blows up exponentially. Mark on enqueue, or on push for a stack.
- Recursive DFS on a big grid. The limit is about 1000 frames. Say the limit, then write or describe the iterative version.
- Mutating the input without asking. Overwriting
'1'with'0'is elegant and may be exactly what the caller did not want. Ask, and know theseenset alternative. - 4 neighbours when the problem said 8, or 8 when it said 4. Minesweeper and Shortest Path in Binary Matrix are 8; Islands and Rotting Oranges are 4. Reread the prompt and say which you are using.
- Flood Fill with
color == oldand no early return: every repainted cell still matches and the loop never ends. - Iterating
for u in graphon a defaultdict built from edges. Isolated nodes are missing, andgraph[u]on a missing key inserts it mid-iteration. Iterate the node list and read with.get. - Adding an undirected edge once, or a directed edge twice. Decide the direction when you write
buildand say it. - Checking the BFS target only when enqueuing neighbours. A start that equals the target (the 1×1 grid) is never checked and you return -1.
- Dijkstra with
(node, cost)tuples. The heap orders by the first element, so the node id drives the order and the algorithm is wrong. Cost first. Also skip stale pops or you will "settle" a node twice. - Saying "O(n²)" for a grid walk without defining n. Say O(rc) or O(cells); the interviewer wants to hear you know it is linear in the input.
Do before marking this topic done
- Blank editor, 10 minutes: the BFS template with distances and the iterative DFS template, once for an adjacency list and once for a grid. Dry-run BFS on a 3×3 grid and write the queue contents after each level.
- Number of Islands, then Max Area of Island, then the variant where diagonals connect, 15 minutes total. State the complexity and whether you mutated the grid before each one.
- Minesweeper from memory, DFS version first, then the BFS version. Dry-run both on the 4×4 board in the diagram with click (3,0) and confirm you get the same "after" board, including the three cells that stay hidden. Then answer: "count the revealed cells" and "did the player win".
- Network: given
links = [('core1', 'agg1'), ('core1', 'agg2'), ('agg1', 'tor1'), ('agg1', 'tor2'), ('agg2', 'tor2'), ('agg2', 'tor3')], writeunreachable_if_down(links, root, x)returning the switches that cannot reachrootwhenxfails. Check:agg1down strandstor1only;agg2down strandstor3only. Then "which single failure strands the most switches", then "hop count fromcore1to every switch". - Rotting Oranges with the fresh counter; dry-run the all-rotten grid (0) and a walled-off fresh orange (-1). Then say in one sentence how it models a failure spreading across a fabric.
- Optional depth: Network Delay Time with heapq from memory, then explain on the A-B-C diagram why BFS gets it wrong and what OSPF would compute.
Practice: linked problems
Logged attempts feed the tracker. Open the problem in a new tab, solve in a blank editor, then log honestly.
- LC 529MinesweeperMedium
- LC 733Flood FillEasy
- LC 133Clone GraphMedium
- LC 200Number of IslandsMedium
- LC 547Number of ProvincesMedium
- LC 695Max Area of IslandMedium
- LC 994Rotting OrangesMedium
- LC 1091Shortest Path in Binary MatrixMedium
- LC 207Course ScheduleMedium
- LC 210Course Schedule IIMedium
- LC 721Accounts MergeMedium
- LC 743Network Delay TimeMedium
- LC 1584Min Cost to Connect All PointsMedium
← 12 · Routing fundamentals, OSPF (vs BGP) and IS-IS · all topics · 14 · BGP in depth →