Breadth-First Search (BFS)
Explore in rings: first everything 1 step away, then everything 2 steps away, and so on. Because of that, the first time BFS reaches a node is guaranteed to be by a shortest route.
1. What is this pattern for?
BFS works on any graph (nodes joined by edges): a road map, a social network, a grid of cells, or an implicit graph where the "nodes" are states (lock combinations, words) and an "edge" is one legal move. It uses a queue: first in, first out, like a line at a coffee shop. Nodes are served in the order they were discovered, which means in order of distance.
Use it for:
- Shortest path / fewest moves when every move costs the same (unweighted).
- Level-by-level processing: "after k minutes", "k degrees of separation".
- Multi-source spreading: fire, infection, rotting, "distance to the nearest X".
- Topological order by peeling (Kahn's algorithm / peeling leaves).
2. How it works (bisected)
- Seed the queue. Put the start node in a
deque, setdist[start] = 0, and mark it seen. - Pop from the front.
u = queue.popleft(). Because of FIFO order,uis always one of the closest nodes not yet expanded. - Discover neighbours. For each neighbour
vnot yet seen: setdist[v] = dist[u] + 1, rememberparent[v] = u, mark it seen right now, and push it to the back. - Repeat until the queue is empty (or until you pop the target). The queue always holds at most two neighbouring distances,
dandd+1. That's why the rings never get out of order. - Need levels? Freeze
size = len(queue)and pop exactlysizenodes per round. Each round is one level (one minute, one move). - Many starting points? Put all of them in the queue at distance 0 before the loop. That's multi-source BFS, and it costs the same as a single BFS.
- Need the path itself? Walk
parentpointers back from the target, then reverse.
3. Seeing a problem: how to spot BFS
Trigger words
- "shortest path", "minimum number of steps / moves / turns / transformations"
- "fewest", "nearest", "closest", "distance to the nearest…"
- "after each minute", "spreads to adjacent", "level by level", "degrees of separation"
- "all of them at the same time" (several sources) → multi-source BFS
- "one letter / one digit / one move at a time" → implicit state graph
Shape of the input
- A graph or grid where every move costs 1 (no weights, or all weights equal).
- A start state and a goal state, plus a rule that generates neighbour states.
- Brute-force smell: trying DFS over all paths and keeping the minimum (exponential), or running a separate BFS from every source (
O(k·(V+E))) when one multi-source BFS would do.
4. When to use it, and when not to
| Use BFS when… | Prefer something else when… |
|---|---|
| Shortest path / fewest moves, every edge costs the same | Edges have different weights: use Dijkstra (heap, see Top K Elements). 0/1 weights: 0-1 BFS with a deque |
| Answers grouped by distance or time (levels) | You just need reachability / components / cycles: DFS is shorter to write and works just as well |
| Several sources spreading at once | You need all paths or all combinations: Backtracking |
| Per-level questions on a binary tree | Order-dependent tree work (inorder, postorder sums): Tree Traversal |
| Grids of cells with 4/8-direction moves | See Matrix Traversal for boundary and direction-vector tricks |
| Very wide but shallow search spaces | The graph is very deep and memory is tight: DFS uses O(depth) memory while BFS uses O(width) |
Cost: O(V + E) time and O(V) space (queue + seen). On an R × C grid that's O(R·C). In implicit state graphs, V = number of reachable states (e.g. 104 lock codes).
5. The Python templates (memorize these)
from collections import deque
# A) Shortest distance from src to every node it can reach (unweighted graph).
# graph is a dict: node -> list of neighbour nodes.
def bfs_distances(graph, src):
# dist[node] = number of edges on the shortest route from src to node.
# It is also our "seen" record: a node counts as seen once it is in dist.
dist = {}
dist[src] = 0
# A deque is a list that is fast at both ends.
# append(x) adds x at the BACK, popleft() removes the item at the FRONT.
queue = deque()
queue.append(src)
while len(queue) > 0:
# Take the node that has waited the longest (FIFO = closest node first).
current = queue.popleft()
for neighbour in graph[current]:
# Only look at neighbours we have never seen before.
if neighbour not in dist:
# Mark it NOW, when it joins the queue, so it can't join twice.
dist[neighbour] = dist[current] + 1
queue.append(neighbour)
return dist
What the code is doing, line by line:
from collections import dequeloads Python's double-ended queue. A plain list is slow at removing from the front; a deque is fast at both ends.dist = {}thendist[src] = 0: the start is 0 steps from itself. Being a key indistalso means "seen", so we don't need a separate set.queue.append(src)puts the start in line.appendadds at the back.while len(queue) > 0:keeps going as long as someone is waiting in line.current = queue.popleft()takes the node at the front. Because nodes join at the back and leave at the front (first in, first out), nodes come out in order of distance: all distance-1 nodes before any distance-2 node.- For each
neighbournot yet indist: its distance is one more thancurrent's, and it joins the back of the queue. We record it the moment it joins, so no node is ever queued twice. - When the queue is empty, every reachable node has its shortest distance in
dist.
Dry run of bfs_distances(graph, 0). Example graph used in every template dry run (from the tests): edges 0-1, 0-2, 1-3, 2-3, 3-4, 4-5, 2-6, so graph = {0: [1, 2], 1: [0, 3], 2: [0, 3, 6], 3: [1, 2, 4], 4: [3, 5], 5: [4], 6: [2]}.
| Pop | New neighbours (distance) | Queue after this node |
|---|---|---|
| (start) | 0 (0) | [0] |
| 0 | 1 (1), 2 (1) | [1, 2] |
| 1 | 3 (2) | [2, 3] |
| 2 | 6 (2) (3 is already seen) | [3, 6] |
| 3 | 4 (3) | [6, 4] |
| 6 | none | [4] |
| 4 | 5 (4) | [5] |
| 5 | none | [] |
Result: {0: 0, 1: 1, 2: 1, 3: 2, 6: 2, 4: 3, 5: 4}.
# B) Level by level: returns the levels, e.g. [[0], [1, 2], [3, 6], ...].
# The level number = distance from src (minutes, moves, tree depth ...).
def bfs_levels(graph, src):
levels = []
# seen = every node that has ever been put in the queue.
seen = set()
seen.add(src)
# append(x) adds at the back, popleft() takes from the front.
queue = deque()
queue.append(src)
while len(queue) > 0:
# Freeze the size: right now the queue holds exactly ONE level.
level_size = len(queue)
level = []
for _ in range(level_size):
current = queue.popleft()
level.append(current)
# (Looking for a target? Here: if current == target: return len(levels))
for neighbour in graph[current]:
if neighbour not in seen:
seen.add(neighbour)
# Added now, popped in the NEXT round, so it is in the next level.
queue.append(neighbour)
levels.append(level)
return levels
What the code is doing, line by line:
levelscollects the answer, one list per level.seenremembers every node that has been queued.- Start with only
srcin the queue and inseen. level_size = len(queue)is the key trick. At the start of each round, the queue holds exactly one whole level. Writing the size down first means the nodes we add during this round wait for the next round.- The
for _ in range(level_size):loop pops exactly that many nodes, adds each one tolevel, and queues its unseen neighbours. (_is the usual name for a loop counter we don't use.) - After the round,
levels.append(level)stores the finished level. The number of finished rounds is the distance, which is how "minutes" or "moves" are counted in problems. - To search for a target instead, return
len(levels)when you pop it, and return -1 after the loop.
Dry run of bfs_levels(graph, 0) on the same graph:
| Round | level_size | Popped this round | Queue at end of round |
|---|---|---|---|
| 1 | 1 | 0 | [1, 2] |
| 2 | 2 | 1, 2 | [3, 6] |
| 3 | 2 | 3, 6 | [4] |
| 4 | 1 | 4 | [5] |
| 5 | 1 | 5 | [] |
Result: [[0], [1, 2], [3, 6], [4], [5]].
# C) The shortest path itself, using parent pointers.
def shortest_path(graph, src, dst):
# parent[node] = the node we came from. The start has no parent (None).
parent = {}
parent[src] = None
# append(x) adds at the back, popleft() takes from the front.
queue = deque()
queue.append(src)
while len(queue) > 0:
current = queue.popleft()
if current == dst:
# The first time we pop dst, we reached it by a shortest route.
# Follow the parent pointers back to src (this gives the path backwards).
backwards = []
step = current
while step is not None:
backwards.append(step)
step = parent[step]
# Reverse it with a while loop so the path reads src -> dst.
path = []
i = len(backwards) - 1
while i >= 0:
path.append(backwards[i])
i = i - 1
return path
for neighbour in graph[current]:
if neighbour not in parent:
parent[neighbour] = current # remember how we got here
queue.append(neighbour)
# The queue ran dry and we never reached dst: there is no path.
return None
What the code is doing, line by line:
parentplays two roles: it is the "seen" record, and it remembers which node led us to each node. The start's parent isNone, meaning "nobody, this is where we began".- The loop is plain BFS. When a new neighbour is found,
parent[neighbour] = currentrecords the step we used to reach it. - When
current == dstis popped, BFS guarantees this route is a shortest one. We walk backwards: dst, its parent, that node's parent, and so on until we hitNone. - That list is backwards (dst first), so a
whileloop copies it from the last item to the first, giving src → dst. - If the queue empties without popping
dst, it can't be reached, so we returnNone.
Dry run of shortest_path(graph, 0, 5):
| Pop | parent entries added | Queue after this node |
|---|---|---|
| 0 | 1←0, 2←0 | [1, 2] |
| 1 | 3←1 | [2, 3] |
| 2 | 6←2 | [3, 6] |
| 3 | 4←3 | [6, 4] |
| 6 | none | [4] |
| 4 | 5←4 | [5] |
| 5 = dst | walk back: 5, 4, 3, 1, 0 | reverse → [0, 1, 3, 4, 5] |
# D) Multi-source BFS: every source starts at distance 0.
def multi_source_bfs(graph, sources):
dist = {}
# append(x) adds at the back, popleft() takes from the front.
queue = deque()
# Put ALL the sources in the queue before the loop starts.
for source in sources:
dist[source] = 0
queue.append(source)
while len(queue) > 0:
current = queue.popleft()
for neighbour in graph[current]:
if neighbour not in dist:
dist[neighbour] = dist[current] + 1
queue.append(neighbour)
# dist[node] = distance from node to its NEAREST source.
return dist
What the code is doing, line by line:
- The only difference from template A is the start. The
for source in sources:loop gives every source distance 0 and puts it in the queue before the main loop. - The main loop is exactly template A. Ripples spread from all sources at once, and a node is first reached by whichever source is closest to it.
- So
dist[node]ends up as the distance to the nearest source, after just one pass over the graph.
Dry run of multi_source_bfs(graph, [0, 5]):
| Pop | New neighbours (distance) | Queue after this node |
|---|---|---|
| (start) | 0 (0), 5 (0) | [0, 5] |
| 0 | 1 (1), 2 (1) | [5, 1, 2] |
| 5 | 4 (1) | [1, 2, 4] |
| 1 | 3 (2) | [2, 4, 3] |
| 2 | 6 (2) | [4, 3, 6] |
| 4 | none (3 and 5 seen) | [3, 6] |
| 3 | none | [6] |
| 6 | none | [] |
Result: {0: 0, 5: 0, 1: 1, 2: 1, 4: 1, 3: 2, 6: 2}. Node 4 is 1 step from source 5, not 3 steps from source 0.
6. How to remember it forever
How this hook works: what “Ripples in a queue” means
What the slogan means, phrase by phrase
- “Ripples”
- BFS spreads out in rings from the start: everything 1 step away, then everything 2 steps away, and so on.
- “in a queue”
- The queue holds the edge of the ripple. First in, first out, so nearer nodes are always processed before farther ones.
- “Mark when you enqueue”
- Stamp a node's ticket as it joins the queue (
dist[neighbour] = …), not when it leaves. Otherwise the same node can join the line many times.
Why this picture fits the pattern: BFS answers "fewest steps" questions in unweighted graphs. A ripple reaches every point by the shortest route first, because it never skips ahead.
The story → the code
| In the story | In the code |
|---|---|
| The pebble | src, dist[src] = 0 |
| Ring number | dist[node] |
| The ripple's front | queue (a deque) |
| Spread one point | current = queue.popleft(), push unseen neighbours |
| Ticket stamped at the door | set dist[neighbour] when appending |
| Several pebbles at once | all sources in the queue at distance 0 |
Watch it on numbers
Graph: 0–1, 0–2, 1–3, 2–3, 3–4. Pebble at 0.
- Ring 0: {0}. Spread 0: stamp 1 and 2 at distance 1.
- Spread 1: stamp 3 at distance 2. Spread 2: 3 is already stamped, so it doesn't join twice.
- Spread 3: stamp 4 at distance 3.
dist = {0: 0, 1: 1, 2: 1, 3: 2, 4: 3}. Each ring is reached by the shortest route.
The trap it saves you from
Marking when you pop instead of when you push: node 3 would be queued by both 1 and 2. On big grids that duplication explodes. Also don't use BFS on weighted edges, which need Dijkstra.
It still works when…
Rotting Oranges: every rotten orange is a pebble dropped at once, and the minutes are the ring numbers. Freeze len(queue) at the start of each round to process exactly one ring.
Picture
Concentric rings around the start, numbered 0, 1, 2, … The ring number is the distance. The queue only ever holds two neighbouring rings at a time.
Chant
"Seed, pop left, stamp and push right. Freeze the size for levels. All sources go in at zero."
DFS vs BFS
"Stack dives, queue ripples." Swap the stack for a queue and a deep explorer becomes a shortest-path finder.
7. LeetCode problems, solved step by step
Medium 994. Rotting Oranges Start here Google Meta Amazon Microsoft
Problem: A grid holds 0 (empty), 1 (fresh orange) and 2 (rotten orange). Every minute, each rotten orange rots its 4-directional fresh neighbours. Return the minutes until no fresh orange is left, or -1 if some can never rot.
Solve it on LeetCode ↗ · Video walkthrough ↗
grid = [[2,1,1],[1,1,0],[0,1,1]]4grid = [[2,1,1],[0,1,1],[1,0,1]]-1Constraints: m == grid.length · n == grid[i].length · 1 ≤ m, n ≤ 10 · grid[i][j] is 0, 1, or 2.
Hint 1
All rotten oranges spread at the same time. That's BFS from many starting points at once.
Hint 2
Put every rotten orange in the queue first; each BFS level is one minute. At the end, any fresh orange left means −1.
From brute force to the pattern: Simulating minute by minute by rescanning the whole grid is O((r · c)²). A multi-source BFS spreads from all rotten oranges together and touches each cell once: O(r · c).
Spot it: "every minute, spreads to adjacent" with many starting rotten oranges = multi-source, level-by-level BFS.
Key insight: Put all rotten oranges in the queue at minute 0. Each frozen-size round is one minute. Count the fresh oranges up front so you can tell at the end whether any were unreachable.
Approach:
- Scan the grid once. Push every rotten orange into the queue and count the fresh ones.
- While the queue is non-empty and fresh oranges remain, process one level (frozen size). That's one minute.
- For each popped cell, rot every fresh 4-neighbour: set it to 2 (mark on enqueue), decrease
fresh, and enqueue it. - After each level, add 1 to
minutes. - Return
minutesiffresh == 0, else -1.
class Solution:
def orangesRotting(self, grid: List[List[int]]) -> int:
rows = len(grid)
cols = len(grid[0])
# The queue holds (row, col) of rotten oranges that will spread next.
# append(x) adds at the back, popleft() takes from the front.
queue = deque()
fresh = 0
# Step 1: scan the grid once. Every rotten orange is a starting point,
# and we count the fresh ones so we know when we are finished.
for row in range(rows):
for col in range(cols):
if grid[row][col] == 2:
queue.append((row, col))
elif grid[row][col] == 1:
fresh = fresh + 1
# The 4 moves as (row change, col change): down, up, right, left.
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
minutes = 0
# Step 2: go on while something can still spread AND something is still fresh.
while len(queue) > 0 and fresh > 0:
# Freeze the size: these are exactly the oranges that spread THIS minute.
level_size = len(queue)
for _ in range(level_size):
cell = queue.popleft()
row = cell[0]
col = cell[1]
for direction in directions:
new_row = row + direction[0]
new_col = col + direction[1]
# Skip cells outside the grid.
if new_row < 0 or new_row >= rows or new_col < 0 or new_col >= cols:
continue
if grid[new_row][new_col] == 1:
grid[new_row][new_col] = 2 # rot it now = mark when enqueued
fresh = fresh - 1
queue.append((new_row, new_col))
# One whole level is done, so one minute has passed.
minutes = minutes + 1
# Step 3: a fresh orange that is left over could never be reached.
if fresh == 0:
return minutes
else:
return -1
What the code is doing, line by line:
rowsandcolsstore the grid size so the bounds checks are easy to read.- Step 1 (scan): the double
forloop visits every cell. Rotten cells (2) go into the queue as(row, col)pairs; fresh cells (1) are only counted infresh. directionslists the 4 moves as (row change, col change). Adding one to a cell's position gives a neighbour.- Step 2 (spread): the
whileloop runs while there are rotten oranges to spread from and fresh oranges left to rot. The second condition stops us counting an extra empty minute at the end. level_size = len(queue)freezes this minute's rotten oranges. Each one is popped and split intorowandcolby indexing (cell[0],cell[1]).- For each direction, out-of-grid neighbours are skipped with
continue. A fresh neighbour is set to 2 straight away (so nobody rots it twice),freshgoes down by 1, and it is queued for the next minute. - After the whole level,
minutesgoes up by 1. - Step 3: if
freshreached 0, return the minutes; otherwise some orange was walled off, so return -1.
Dry run on grid = [[2,1,1],[1,1,0],[0,1,1]] (answer 4):
| Minute | Popped (spreading) | Newly rotten | fresh after | Queue after the minute |
|---|---|---|---|---|
| (scan) | - | - | 6 | [(0,0)] |
| 1 | (0,0) | (1,0), (0,1) | 4 | [(1,0), (0,1)] |
| 2 | (1,0), (0,1) | (1,1), (0,2) | 2 | [(1,1), (0,2)] |
| 3 | (1,1), (0,2) | (2,1) | 1 | [(2,1)] |
| 4 | (2,1) | (2,2) | 0 | [(2,2)] |
fresh is now 0, so the loop stops with minutes = 4 and the code returns 4.
Edge cases to test:
- No fresh oranges at the start → 0.
- A fresh orange that can never be reached → −1.
- No rotten oranges but some fresh ones → −1.
Complexity: O(R·C) time and space.
Medium 752. Open the Lock Amazon Microsoft
Problem: A 4-wheel lock starts at "0000". One move turns one wheel up or down by one (9 wraps to 0). Avoid every code in deadends. Return the minimum number of moves to reach target, or -1.
deadends = ["0201","0101","0102","1212","2002"], target = "0202"6deadends = ["8888"], target = "0009"1Constraints: 1 ≤ deadends.length ≤ 500 · deadends[i].length == 4 · target.length == 4 · target will not be in the list deadends. · target and deadends[i] consist of digits only.
Hint 1
Each lock state is a node; turning one wheel by one notch is an edge.
Hint 2
BFS from "0000", skipping dead ends and visited states; each level is one more move.
From brute force to the pattern: DFS over all combinations doesn't find the minimum number of moves. BFS over the 10,000 states, each with 8 neighbours, finds it in O(10⁴ · 8).
Spot it: "minimum number of turns", and each turn costs 1. The graph is implicit: nodes are the 104 codes, and every code has 8 neighbours.
Key insight: Generate neighbours on the fly and treat deadends as pre-visited walls. Check whether "0000" itself is a deadend.
Approach:
- Put the deadends in a set. If
"0000"is a deadend, return -1. - BFS from
("0000", 0)with aseenset. - Pop a code. If it's the target, return the number of turns.
- Generate its 8 neighbours (each of 4 wheels, ±1 with wrap-around), and enqueue the ones not seen and not dead.
- If the queue empties, return -1.
class Solution:
def openLock(self, deadends: List[str], target: str) -> int:
# A set makes "is this code a deadend?" an instant check.
dead = set(deadends)
if "0000" in dead:
return -1 # stuck before the first move
seen = set()
seen.add("0000")
# The queue holds (code, turns used to reach it).
# append(x) adds at the back, popleft() takes from the front.
queue = deque()
queue.append(("0000", 0))
while len(queue) > 0:
pair = queue.popleft()
code = pair[0]
turns = pair[1]
if code == target:
return turns # first time we pop the target = fewest turns
# Build the 8 neighbours: each of the 4 wheels turned up (+1) or down (-1).
for i in range(4):
digit = int(code[i])
for step in [1, -1]:
# % 10 wraps around: 9 + 1 becomes 0, and 0 - 1 becomes 9.
new_digit = (digit + step) % 10
next_code = code[:i] + str(new_digit) + code[i + 1:]
if next_code not in seen and next_code not in dead:
seen.add(next_code) # mark when enqueued
queue.append((next_code, turns + 1))
# Every reachable code was tried and none was the target.
return -1
What the code is doing, line by line:
dead = set(deadends)turns the list into a set, so "is this a deadend?" takes constant time.- If
"0000"itself is dead we can't move at all, so return -1. - The queue holds pairs
(code, turns). Each pop is split by indexing:pair[0]is the code,pair[1]the number of turns used. - If the popped code is the target, BFS guarantees
turnsis the minimum, so return it. - Otherwise build the 8 neighbours. For each wheel
i(0 to 3) and eachstep(+1 or -1),(digit + step) % 10gives the new digit with wrap-around, andcode[:i] + str(new_digit) + code[i + 1:]glues the string back together with that one digit changed. - A neighbour that is neither seen nor dead is marked seen and queued with
turns + 1. - If the queue empties, the target can't be reached: return -1.
Dry run on deadends = ["0201","0101","0102","1212","2002"], target = "0202" (answer 6). The queue gets large, so this table counts codes per BFS level:
| turns | Codes popped at this level | Notes |
|---|---|---|
| 0 | 1 | "0000" |
| 1 | 8 | "1000", "9000", "0100", "0900", "0010", "0090", "0001", "0009" |
| 2 | 31 | 32 codes are 2 turns away, but deadend "0101" is never queued |
| 3 | 86 | |
| 4 | 190 | |
| 5 | 356 | |
| 6 | 150 | the 150th code popped at this level is "0202" → return 6 |
Every 4-turn route (like "0000" → "0100" → "0200" → "0201" → "0202") passes through a deadend ("0101", "0201" or "0102"), which is why the answer is 6 turns instead of 4.
Edge cases to test:
"0000"itself is a dead end → −1.- The target is
"0000"→ 0. - Wrap-around between 9 and 0.
Complexity: O(104 · 8 · 4) time, i.e. constant for this fixed lock; O(104) space.
Medium 310. Minimum Height Trees Google Amazon Microsoft
Problem: Given a tree of n nodes (as undirected edges), return every node that, when chosen as the root, gives the minimum possible height.
n = 4, edges = [[1,0],[1,2],[1,3]][1]n = 6, edges = [[3,0],[3,1],[3,2],[3,4],[5,4]][3,4]Constraints: 1 ≤ n ≤ 2 * 10⁴ · edges.length == n - 1 · 0 ≤ ai, bi < n · ai != bi · All the pairs (ai, bi) are distinct. · The given input is guaranteed to be a tree and there will be no repeated edges.
Hint 1
The best roots are in the "middle" of the tree.
Hint 2
Peel off leaves layer by layer (BFS on degrees); the last 1 or 2 nodes left are the answer.
From brute force to the pattern: Running a BFS from every node to measure its height is O(n²). Trimming leaves inward, like a topological sort, finds the centre in O(n).
Spot it: Running a BFS from every node is O(n²). The best roots are the centre of the tree, and you can find the centre with a BFS that runs from the outside in.
Key insight: Multi-source BFS starting from all leaves. Peel off one layer of leaves per round. Nodes whose degree drops to 1 become the next layer. When 2 or fewer nodes remain, those are the centres (a tree has one or two of them).
Approach:
- If
n <= 2, every node is a centre. - Build adjacency sets (removal is easy) and put every leaf (degree 1) in a queue.
- While more than 2 nodes remain, subtract the number of leaves in this layer, then remove each one.
- Removing a leaf detaches it from its only neighbour. If that neighbour's degree drops to 1, it joins the next layer.
- The 1 or 2 nodes left at the end are the answer.
class Solution:
def findMinHeightTrees(self, n: int, edges: List[List[int]]) -> List[int]:
# With 1 or 2 nodes, every node is a centre.
if n <= 2:
answer = []
for node in range(n):
answer.append(node)
return answer
# graph[node] = set of its neighbours. A set makes removing one easy.
graph = {}
for node in range(n):
graph[node] = set()
for edge in edges:
first = edge[0]
second = edge[1]
graph[first].add(second)
graph[second].add(first)
# The outer layer = every leaf (a node with exactly one neighbour).
# append(x) adds at the back, popleft() takes from the front.
leaves = deque()
for node in range(n):
if len(graph[node]) == 1:
leaves.append(node)
remaining = n
# Peel one layer of leaves per round until only 1 or 2 nodes are left.
while remaining > 2:
layer_size = len(leaves)
remaining = remaining - layer_size
for _ in range(layer_size):
leaf = leaves.popleft()
# A leaf has exactly one neighbour. pop() takes it out of the set.
neighbour = graph[leaf].pop()
# Remove the edge from the neighbour's side too.
graph[neighbour].remove(leaf)
if len(graph[neighbour]) == 1:
# The neighbour has just become a leaf: it belongs to the next layer.
leaves.append(neighbour)
# The nodes still in the queue are the centres.
answer = []
for node in leaves:
answer.append(node)
answer.sort() # any order is accepted; sorted is easier to read
return answer
What the code is doing, line by line:
- With 1 or 2 nodes, every node is a centre, so we return
[0]or[0, 1], built with a loop. graphmaps each node to a set of neighbours. Every edge[first, second]is added both ways because the tree is undirected.- The first
leavesqueue holds every node with exactly one neighbour: the outer layer of the tree. remainingcounts nodes not yet peeled. While more than 2 remain, we take a whole layer:layer_sizeis frozen and subtracted fromremaining.- For each leaf,
graph[leaf].pop()removes and returns its only neighbour, andgraph[neighbour].remove(leaf)removes the edge from the other side. - If the neighbour is down to one neighbour, it has become a leaf of the smaller tree, so it joins the queue for the next layer.
- When 2 or fewer nodes remain, the queue holds exactly those centres. We copy them into a list and sort it.
Dry run on n = 6, edges = [[3,0],[3,1],[3,2],[3,4],[5,4]] (answer [3, 4]). Node 3 has neighbours {0, 1, 2, 4}; node 4 has {3, 5}.
| Round | remaining (before → after) | Leaf popped | Its neighbour's degree after | leaves queue |
|---|---|---|---|---|
| (start) | 6 | - | - | [0, 1, 2, 5] |
| 1 | 6 → 2 | 0 | 3 has 3 left | [1, 2, 5] |
| 1 | 1 | 3 has 2 left | [2, 5] | |
| 1 | 2 | 3 has 1 left → queued | [5, 3] | |
| 1 | 5 | 4 has 1 left → queued | [3, 4] |
remaining = 2, so the loop stops and the answer is [3, 4].
Edge cases to test:
n = 1→[0].n = 2→[0, 1].- A star-shaped tree → the centre node.
Complexity: O(n) time and space.
Hard 127. Word Ladder Google Meta Amazon Cisco Apple
Problem: Transform beginWord into endWord by changing one letter at a time, where every intermediate word must be in wordList. Return the number of words in the shortest sequence (including both ends), or 0.
Solve it on LeetCode ↗ · Video walkthrough ↗
beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]5beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"]0Constraints: 1 ≤ beginWord.length ≤ 10 · endWord.length == beginWord.length · 1 ≤ wordList.length ≤ 5000 · wordList[i].length == beginWord.length · beginWord, endWord, and wordList[i] consist of lowercase English letters. · beginWord != endWord · All the words in wordList are unique.
Hint 1
Words are nodes; two words are connected if they differ by exactly one letter. You want the shortest path.
Hint 2
BFS from beginWord. To find neighbours fast, try each position × each letter, or group words by patterns like h*t.
From brute force to the pattern: Comparing every pair of words to build the graph is O(n² · L). Generating neighbours with wildcard patterns plus BFS finds the shortest chain in O(n · L²).
Spot it: "shortest transformation sequence" + "one letter at a time" = BFS on an implicit graph of words.
Key insight: Comparing every pair of words is slow. Instead, bucket words by wildcard patterns: hot belongs to *ot, h*t, ho*. Two words are neighbours if they share a bucket. Empty each bucket after its first use, because later visits can't produce a shorter path.
Approach:
- If
endWordisn't in the word list, return 0. - Bucket every word by its wildcard patterns, e.g.
hot→*ot,h*t,ho*. - BFS from
(beginWord, 1). Pop a word and return its step count if it'sendWord. - For each of its patterns, enqueue every unseen word in that bucket with steps + 1, then empty the bucket.
- If the queue empties, return 0.
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
words = set(wordList)
if endWord not in words:
return 0
# buckets["h*t"] = every word that fits h?t, for example ["hot", "hit"].
# Two words in the same bucket differ in exactly one letter.
all_words = []
for word in wordList:
all_words.append(word)
if beginWord not in words:
all_words.append(beginWord)
buckets = {}
for word in all_words:
for i in range(len(word)):
pattern = word[:i] + "*" + word[i + 1:]
if pattern not in buckets:
buckets[pattern] = []
buckets[pattern].append(word)
seen = set()
seen.add(beginWord)
# The queue holds (word, number of words in the sequence so far).
# append(x) adds at the back, popleft() takes from the front.
queue = deque()
queue.append((beginWord, 1))
while len(queue) > 0:
pair = queue.popleft()
word = pair[0]
steps = pair[1]
if word == endWord:
return steps
for i in range(len(word)):
pattern = word[:i] + "*" + word[i + 1:]
for next_word in buckets[pattern]:
if next_word not in seen:
seen.add(next_word)
queue.append((next_word, steps + 1))
# Empty the bucket: all its words are queued now, a later visit can't help.
buckets[pattern] = []
return 0
What the code is doing, line by line:
- If
endWordisn't in the word list, no sequence can end there: return 0. all_wordsis the word list plusbeginWord(if it isn't already in it).- For every word and every position
i,word[:i] + "*" + word[i + 1:]replaces letteriwith*. That pattern is the bucket key. If the key is new, we create an empty list first, then add the word. - BFS starts at
(beginWord, 1), because the answer counts words, and the first word counts as 1. - Each pop is split into
word = pair[0]andsteps = pair[1]. If it'sendWord, returnsteps. - For each of the word's patterns, every unseen word in that bucket is one letter away: mark it and queue it with
steps + 1. buckets[pattern] = []empties the bucket. Everything in it is already queued, so looking at it again later would be wasted work.
Dry run on beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"] (answer 5):
| Pop (word, steps) | Buckets that give new words | Queued | Queue after this word |
|---|---|---|---|
| ("hit", 1) | "h*t" = [hot, hit] | hot | [(hot, 2)] |
| ("hot", 2) | "*ot" = [hot, dot, lot] | dot, lot | [(dot, 3), (lot, 3)] |
| ("dot", 3) | "do*" = [dot, dog] | dog | [(lot, 3), (dog, 4)] |
| ("lot", 3) | "lo*" = [lot, log] | log | [(dog, 4), (log, 4)] |
| ("dog", 4) | "*og" = [dog, log, cog] | cog | [(log, 4), (cog, 5)] |
| ("log", 4) | none ("*og" already emptied) | - | [(cog, 5)] |
| ("cog", 5) | it's endWord → return 5 (hit → hot → dot → dog → cog) | ||
Edge cases to test:
endWordnot in the list → 0.beginWordone step fromendWord→ 2.- Disconnected words → 0.
Complexity: O(N · L²) time (N words of length L, building the strings), O(N · L²) space. Bidirectional BFS (from both ends) speeds it up further in practice.
Hard 815. Bus Routes Google Amazon
Problem: routes[i] is the loop of stops bus i visits. Starting at stop source, what's the fewest number of buses you must take to reach target? Return -1 if it's impossible.
routes = [[1,2,7],[3,6,7]], source = 1, target = 62routes = [[7,12],[4,5,15],[6],[15,19],[9,12,13]], source = 15, target = 12-1Constraints: 1 ≤ routes.length ≤ 500. · 1 ≤ routes[i].length ≤ 10⁵ · All the values of routes[i] are unique. · sum(routes[i].length) ≤ 10⁵ · 0 ≤ routes[i][j] < 10⁶ · 0 ≤ source, target < 10⁶
Hint 1
Count buses, not stops. Which stops connect one bus to another?
Hint 2
BFS where each level boards one more bus: from a stop, take every unused route through it and visit all its stops.
From brute force to the pattern: BFS over stops, one stop per step, counts stops instead of buses and is slow. BFS by routes, marking each route as used once, gives the minimum number of buses in O(total stops).
Spot it: "fewest buses" = BFS, but the cost is per bus, not per stop. Each BFS level must be "one more bus".
Key insight: Expand a whole bus at once. From a stop, every unused bus through it reaches all of that bus's stops at cost +1. Mark buses as used so each route is expanded only once. Otherwise long routes make it O(stops²).
Approach:
- If
source == target, return 0. - Map each stop to the buses that serve it.
- BFS over
(stop, buses_taken), starting at(source, 0). - From a stop, take every bus not used yet: mark it used, and reach all of its stops at
buses + 1. - Return as soon as one of those stops is the target. If the queue empties, return -1.
class Solution:
def numBusesToDestination(self, routes: List[List[int]], source: int, target: int) -> int:
if source == target:
return 0
# stop_to_buses[stop] = list of the bus numbers that visit this stop.
stop_to_buses = {}
for bus in range(len(routes)):
for stop in routes[bus]:
if stop not in stop_to_buses:
stop_to_buses[stop] = []
stop_to_buses[stop].append(bus)
used_bus = set() # buses we have already ridden
seen_stop = set() # stops we have already put in the queue
seen_stop.add(source)
# The queue holds (stop, buses taken to get here).
# append(x) adds at the back, popleft() takes from the front.
queue = deque()
queue.append((source, 0))
while len(queue) > 0:
pair = queue.popleft()
stop = pair[0]
buses = pair[1]
if stop not in stop_to_buses:
continue # no bus serves this stop
for bus in stop_to_buses[stop]:
if bus in used_bus:
continue # each bus only needs to be ridden once
used_bus.add(bus)
# Riding this bus reaches ALL of its stops for one more bus.
for next_stop in routes[bus]:
if next_stop == target:
return buses + 1
if next_stop not in seen_stop:
seen_stop.add(next_stop)
queue.append((next_stop, buses + 1))
return -1
What the code is doing, line by line:
- If we're already at the target, we need 0 buses.
stop_to_busesis built with two loops: for every bus number and every stop on its route, add the bus to that stop's list (creating the list the first time we see the stop).used_busremembers buses we've already ridden;seen_stopremembers stops already queued.- The queue holds
(stop, buses). Each pop is split by indexing. A stop no bus visits is skipped. - For each unused bus at this stop, mark it used and walk its whole route. Every stop on it costs
buses + 1. If one of them is the target, return right away; otherwise queue new stops. - Marking the bus as used is what keeps this fast: each route is scanned once, however many of its stops we later pop.
Dry run on routes = [[1,2,7],[3,6,7]], source = 1, target = 6 (answer 2). stop_to_buses = {1: [0], 2: [0], 7: [0, 1], 3: [1], 6: [1]}.
| Pop (stop, buses) | Bus ridden | Stops reached | Queue after |
|---|---|---|---|
| (1, 0) | bus 0 | 1 (seen), 2, 7 | [(2, 1), (7, 1)] |
| (2, 1) | none (bus 0 already used) | - | [(7, 1)] |
| (7, 1) | bus 1 (bus 0 skipped) | 3 → queued, 6 = target | return 1 + 1 = 2 |
Edge cases to test:
source == target→ 0.- The target is unreachable → −1.
- A stop shared by many routes.
Complexity: O(Σ len(routes[i])) time and space: each bus and each stop is expanded once.
8. Common mistakes
append.pop(0)list.pop(0) is O(n). Use collections.deque and popleft(), which is O(1).k times faster and simpler.while len(queue) > 0 and fresh > 0).9. Self-check quiz
When should a node be marked as seen in BFS?
O(V + E)."Fill each empty room with the distance to its nearest gate" (Walls and Gates). Best approach?
O(R·C) pass.Shortest travel time on a road network where each road has a different length?
To process exactly one level per round, the inner loop runs how many times?
10. Practice list (in order)
| # | Problem | Level | Data structure | Twist |
|---|---|---|---|---|
| 111 | Minimum Depth of Binary Tree | Easy | Warm-up: stop at the first leaf you pop | |
| 637 | Average of Levels in Binary Tree | Easy | Level-order BFS; average each level's values | |
| 1091 | Shortest Path in Binary Matrix | Medium | 8-direction grid BFS, count cells | |
| 994 | Rotting Oranges | Medium | Multi-source + levels = minutes | |
| 542 | 01 Matrix | Medium | Multi-source from every 0 | |
| 286 | Walls and Gates | Medium | Multi-source from gates, in place | |
| 752 | Open the Lock | Medium | Implicit graph of 104 codes | |
| 310 | Minimum Height Trees | Medium | Peel leaves from the outside in | |
| 1926 | Nearest Exit from Entrance in Maze | Medium | Target = any border cell | |
| 117 | Populating Next Right Pointers in Each Node II | Medium | Level-order BFS; link each node to the next one popped on its level | |
| 433 | Minimum Genetic Mutation | Medium | Word Ladder on 8-letter genes over the alphabet A, C, G, T | |
| 909 | Snakes and Ladders | Medium | BFS over square numbers; the hard part is square → (row, col) | |
| 863 | All Nodes Distance K in Binary Tree | Medium | Add parent links so the tree becomes an undirected graph; BFS k steps from target | |
| 934 | Shortest Bridge | Medium | DFS to mark one island, then multi-source BFS from all its cells until the other island is touched | |
| 1129 | Shortest Path with Alternating Colors | Medium | BFS over (node, colour of the last edge) | |
| 3629 | Minimum Jumps to Reach End via Prime Teleportation | Medium | BFS; a prime p teleports to every index whose value p divides — use each prime's list once, then clear it | |
| 1162 | As Far from Land as Possible | Medium | Multi-source BFS from all land cells; the last water cell reached is the farthest | |
| 1236 | Web Crawler | Medium | BFS over links, keeping only URLs with the start URL's hostname | |
| 1311 | Get Watched Videos by Your Friends | Medium | BFS exactly level layers from id; count videos of that layer; sort by (frequency, name) | |
| 3552 | Grid Teleportation Traversal | Medium | 0-1 BFS: moves cost 1, teleports cost 0; each letter's portals are expanded once | |
| 1197 | Minimum Knight Moves | Medium | BFS from (0, 0) toward (|x|, |y|) using symmetry; allow a small margin below zero | |
| 127 | Word Ladder | Hard | Wildcard buckets; bidirectional BFS | |
| 815 | Bus Routes | Hard | BFS over buses, not stops | |
| 847 | Shortest Path Visiting All Nodes | Hard | State = (node, visited bitmask) | |
| 1293 | Shortest Path in a Grid with Obstacles Elimination | Hard | State = (r, c, removals left) | |
| 126 | Word Ladder II | Hard | BFS layer by layer recording every parent on a shortest path; then backtrack from endWord to list all ladders | |
| 864 | Shortest Path to Get All Keys | Hard | BFS over (row, col, keys bitmask): the same cell with different keys is a different state | |
| 1345 | Jump Game IV | Hard | BFS where equal values are connected; clear each value's index list after its first use | |
| 2045 | Second Minimum Time to Reach Destination | Hard | BFS keeping the two smallest distinct edge counts per node; convert the count to time with the signals | |
| 854 | K-Similar Strings | Hard | BFS over strings; from each, only swap the first mismatch with a position that fixes it |
Worked solutions for the practice list
Problems already solved in section 7 link back up there. Everything else is solved here.
Easy 111. Minimum Depth of Binary Tree Start here Google Amazon
Problem: Return the number of nodes on the shortest path from the root down to a leaf (a node with no children).
root = [3,9,20,null,null,15,7]2root = [2,null,3,null,4,null,5,null,6]5Constraints: The number of nodes in the tree is in the range [0, 10⁵]. · -1000 ≤ Node.val ≤ 1000
Hint 1
The minimum depth is the distance to the nearest leaf.
Hint 2
BFS level by level and stop at the first leaf you meet.
From brute force to the pattern: DFS must explore the whole tree to be sure of the minimum; it also has a trap: a node with one child is not a leaf. BFS stops at the shallowest leaf, often without visiting most of the tree.
Spot it: "minimum depth" is the shortest distance. BFS reaches shallow nodes first, so the first leaf it pops is the answer and it can stop early.
Approach:
- If the tree is empty, return 0.
- BFS with the queue holding
(node, depth), starting at(root, 1). - Pop a node. If it has no children, return its depth.
- Otherwise enqueue its children with
depth + 1.
class Solution:
def minDepth(self, root: Optional[TreeNode]) -> int:
if root is None:
return 0
# The queue holds (node, depth of that node). The root has depth 1.
# append(x) adds at the back, popleft() takes from the front.
queue = deque()
queue.append((root, 1))
while len(queue) > 0:
pair = queue.popleft()
node = pair[0]
depth = pair[1]
# A leaf has no children. The first leaf we pop is the shallowest one.
if node.left is None and node.right is None:
return depth
if node.left is not None:
queue.append((node.left, depth + 1))
if node.right is not None:
queue.append((node.right, depth + 1))
return 0 # never reached for a non-empty tree
What the code is doing, line by line:
- An empty tree has depth 0.
- The queue holds
(node, depth)pairs, starting with(root, 1). - Each pop is split into
node = pair[0]anddepth = pair[1]. - If the node has no left and no right child, it is a leaf. BFS pops nodes in depth order, so the first leaf we meet is the shallowest: return its depth right away.
- Otherwise queue each child that exists, one level deeper.
Dry run on root = [3,9,20,null,null,15,7] (answer 2):
| Pop (node, depth) | Leaf? | Queue after |
|---|---|---|
| (3, 1) | no, children 9 and 20 | [(9, 2), (20, 2)] |
| (9, 2) | yes → return 2 | [(20, 2)] (never looked at) |
Nodes 15 and 7 are never visited. That's the advantage over a DFS that would explore the whole tree.
Edge cases to test:
- An empty tree → 0.
- A chain of single children, e.g.
[2, null, 3, null, 4]→ 3 (not 1). - A single node → 1.
Complexity: O(n) worst case, but it stops at the first leaf. O(w) space.
Medium 1091. Shortest Path in Binary Matrix Google Meta Amazon Microsoft
Problem: In an n × n grid of 0s (open) and 1s (blocked), find the length (number of cells) of the shortest path from the top-left to the bottom-right, moving in any of the 8 directions. Return -1 if there's no path.
grid = [[0,1],[1,0]]2grid = [[0,0,0],[1,1,0],[1,1,0]]4Constraints: n == grid.length · n == grid[i].length · 1 ≤ n ≤ 100 · grid[i][j] is 0 or 1
Hint 1
Every move costs 1, and you may move in 8 directions.
Hint 2
BFS from (0, 0) through cells containing 0, counting path length; check the start and end cells first.
From brute force to the pattern: DFS tries many paths and doesn't naturally give the shortest. BFS reaches each cell first along a shortest path: O(n²).
Spot it: "shortest clear path" where every move costs 1, so grid BFS.
Approach:
- If the start or end cell is blocked, return -1.
- Enqueue
(0, 0, 1), since the path length counts cells, and mark the start (set it to 1). - Pop a cell. If it's the bottom-right, return its distance.
- For all 8 neighbours that are in bounds and open: mark them, then enqueue them with distance + 1.
class Solution:
def shortestPathBinaryMatrix(self, grid: List[List[int]]) -> int:
size = len(grid)
last = size - 1
# A blocked start or a blocked end means there is no path at all.
if grid[0][0] == 1 or grid[last][last] == 1:
return -1
# The queue holds (row, col, cells on the path so far). The start counts as 1.
# append(x) adds at the back, popleft() takes from the front.
queue = deque()
queue.append((0, 0, 1))
grid[0][0] = 1 # reuse the grid as "visited": 1 = blocked or visited
while len(queue) > 0:
item = queue.popleft()
row = item[0]
col = item[1]
length = item[2]
if row == last and col == last:
return length
# All 8 directions: the row and the col each change by -1, 0 or +1.
for row_change in [-1, 0, 1]:
for col_change in [-1, 0, 1]:
new_row = row + row_change
new_col = col + col_change
# Skip cells outside the grid.
if new_row < 0 or new_row >= size or new_col < 0 or new_col >= size:
continue
if grid[new_row][new_col] == 0:
grid[new_row][new_col] = 1 # mark on enqueue
queue.append((new_row, new_col, length + 1))
return -1
What the code is doing, line by line:
sizeis the grid width,last = size - 1the index of the last row/column.- If the start or the end cell is blocked, return -1 immediately.
- The queue holds
(row, col, length), wherelengthcounts cells on the path, so the start is 1. Settinggrid[0][0] = 1marks it as visited (we reuse the grid instead of a separate set). - Each pop is split into
row,col,lengthby indexing. Popping the bottom-right cell means we're done. - The two nested loops over
[-1, 0, 1]try all 8 neighbours (the "no move" case lands on the current cell, which is already marked, so it's skipped). - Cells outside the grid are skipped. An open cell (0) is marked 1 and queued with
length + 1.
Dry run on grid = [[0,0,0],[1,1,0],[1,1,0]] (answer 4):
| Pop (row, col, length) | Open neighbours queued | Queue after |
|---|---|---|
| (0, 0, 1) | (0, 1) | [(0,1,2)] |
| (0, 1, 2) | (0, 2), (1, 2) | [(0,2,3), (1,2,3)] |
| (0, 2, 3) | none (all marked or blocked) | [(1,2,3)] |
| (1, 2, 3) | (2, 2) | [(2,2,4)] |
| (2, 2, 4) | bottom-right → return 4 | |
Edge cases to test:
- The start or end cell is 1 → −1.
- A 1 × 1 grid with a 0 → 1.
- Diagonal shortcuts.
Complexity: O(n²) time and space.
Medium 542. 01 Matrix Google Amazon Microsoft
Problem: For every cell of a 0/1 matrix, return the distance to the nearest 0 (moving up, down, left or right).
mat = [[0,0,0],[0,1,0],[0,0,0]][[0,0,0],[0,1,0],[0,0,0]]mat = [[0,0,0],[0,1,0],[1,1,1]][[0,0,0],[0,1,0],[1,2,1]]Constraints: m == mat.length · n == mat[i].length · 1 ≤ m, n ≤ 10⁴ · 1 ≤ m * n ≤ 10⁴ · mat[i][j] is either 0 or 1. · There is at least one 0 in mat.
Hint 1
Running a BFS from every 1 is slow. Flip the problem around.
Hint 2
Multi-source BFS starting from all 0s at once; each cell's first visit gives its distance.
From brute force to the pattern: A BFS per cell is O((r · c)²). One multi-source BFS from every zero computes every distance in O(r · c) (a two-pass DP also works).
Spot it: "distance to the nearest X" for every cell is multi-source BFS starting from all the 0s at once.
Approach:
- Make
distfilled with -1, meaning unknown. - Every 0 cell gets
dist = 0and goes into the queue. - Pop a cell. Each neighbour with unknown distance gets
dist[cell] + 1and is enqueued. - Each cell is first reached by its nearest 0, so it's final the moment it's set.
class Solution:
def updateMatrix(self, mat: List[List[int]]) -> List[List[int]]:
rows = len(mat)
cols = len(mat[0])
# dist starts full of -1, which means "not reached yet".
dist = []
for row in range(rows):
dist_row = [-1] * cols # a list holding cols copies of -1
dist.append(dist_row)
# append(x) adds at the back, popleft() takes from the front.
queue = deque()
# Every 0 is a source at distance 0.
for row in range(rows):
for col in range(cols):
if mat[row][col] == 0:
dist[row][col] = 0
queue.append((row, col))
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
while len(queue) > 0:
cell = queue.popleft()
row = cell[0]
col = cell[1]
for direction in directions:
new_row = row + direction[0]
new_col = col + direction[1]
if new_row < 0 or new_row >= rows or new_col < 0 or new_col >= cols:
continue
if dist[new_row][new_col] == -1:
# The first time a cell is reached, it is by its nearest 0.
dist[new_row][new_col] = dist[row][col] + 1
queue.append((new_row, new_col))
return dist
What the code is doing, line by line:
distis built row by row. Each row is[-1] * cols, a list ofcolscopies of -1, meaning "not reached yet".- The double loop finds every 0. Each gets
dist = 0and goes into the queue: all zeros are sources (multi-source BFS). - Each pop is split into
rowandcol. For each of the 4 neighbours inside the grid that is still -1, its distance is the popped cell's distance + 1, and it's queued. - Because all zeros started together, the first ripple to reach a cell comes from its nearest zero, so a cell's distance never needs to change once set.
Dry run on mat = [[0,0,0],[0,1,0],[1,1,1]] (answer [[0,0,0],[0,1,0],[1,2,1]]). Starting queue: the five zeros (0,0), (0,1), (0,2), (1,0), (1,2).
| Pop | Newly set | Queue after |
|---|---|---|
| (0,0) | none | [(0,1), (0,2), (1,0), (1,2)] |
| (0,1) | (1,1) = 1 | [(0,2), (1,0), (1,2), (1,1)] |
| (0,2) | none | [(1,0), (1,2), (1,1)] |
| (1,0) | (2,0) = 1 | [(1,2), (1,1), (2,0)] |
| (1,2) | (2,2) = 1 | [(1,1), (2,0), (2,2)] |
| (1,1) | (2,1) = 2 | [(2,0), (2,2), (2,1)] |
| (2,0), (2,2), (2,1) | none | [] |
Edge cases to test:
- A single 0 in a corner.
- Every cell is 0 → all distances 0.
- Large grids (avoid the per-cell BFS).
Complexity: O(R·C) time and space.
Medium 286. Walls and Gates Google Meta Amazon
Problem: The grid holds -1 (wall), 0 (gate) and INF = 2147483647 (empty room). Fill each empty room in place with the distance to its nearest gate, and leave INF where no gate is reachable.
Solve it on LeetCode ↗ (Premium)
rooms = [[INF,-1,0,INF],[INF,INF,INF,-1],[INF,-1,INF,-1],[0,-1,INF,INF]][[3,-1,0,1],[2,2,1,-1],[1,-1,2,-1],[0,-1,3,4]]rooms = [[-1]][[-1]]Constraints: m == rooms.length, n == rooms[i].length · 1 ≤ m, n ≤ 250 · rooms[i][j] is -1, 0, or 2³¹ − 1
Hint 1
Searching from every room to its nearest gate repeats a lot of work.
Hint 2
Multi-source BFS from all gates at once; fill each room the first time it's reached.
From brute force to the pattern: A BFS from every empty room is O((r · c)²). Starting from all gates together means each room is reached first by its nearest gate: O(r · c).
Spot it: "nearest gate" for every room is multi-source BFS from all the gates.
Approach:
- Enqueue every gate.
- Pop a cell. For each 4-neighbour that's still
INF, set it to the current cell's value + 1 and enqueue it. - Walls are never
INFand gates are 0, so the== INFcheck skips both. It also acts as the visited check.
class Solution:
def wallsAndGates(self, rooms: List[List[int]]) -> None:
INF = 2147483647 # the value LeetCode uses for an empty room
if len(rooms) == 0:
return
rows = len(rooms)
cols = len(rooms[0])
# append(x) adds at the back, popleft() takes from the front.
queue = deque()
# Every gate (0) is a source.
for row in range(rows):
for col in range(cols):
if rooms[row][col] == 0:
queue.append((row, col))
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
while len(queue) > 0:
cell = queue.popleft()
row = cell[0]
col = cell[1]
for direction in directions:
new_row = row + direction[0]
new_col = col + direction[1]
if new_row < 0 or new_row >= rows or new_col < 0 or new_col >= cols:
continue
# Only empty, unvisited rooms are still INF (walls are -1, gates are 0).
if rooms[new_row][new_col] == INF:
rooms[new_row][new_col] = rooms[row][col] + 1
queue.append((new_row, new_col))
What the code is doing, line by line:
INFis the special "empty room" value from the problem. An empty grid has nothing to do.- The double loop queues every gate (value 0). They are all sources at distance 0.
- Each pop is split into
rowandcol. Out-of-grid neighbours are skipped. - Only a neighbour that is still
INFis updated. Walls (-1) and gates (0) never equalINF, and a room we've already filled no longer does either, so this one check means "unvisited empty room". - The room gets the popped cell's value + 1 and is queued. The grid is changed in place, so nothing is returned.
Dry run on the LeetCode example (I = INF): [[I,-1,0,I],[I,I,I,-1],[I,-1,I,-1],[0,-1,I,I]]. Starting queue: gates (0,2), (3,0).
| Pop | Rooms filled | Queue after |
|---|---|---|
| (0,2) | (1,2) = 1, (0,3) = 1 | [(3,0), (1,2), (0,3)] |
| (3,0) | (2,0) = 1 | [(1,2), (0,3), (2,0)] |
| (1,2) | (2,2) = 2, (1,1) = 2 | [(0,3), (2,0), (2,2), (1,1)] |
| (0,3) | none | [(2,0), (2,2), (1,1)] |
| (2,0) | (1,0) = 2 | [(2,2), (1,1), (1,0)] |
| (2,2) | (3,2) = 3 | [(1,1), (1,0), (3,2)] |
| (1,1) | none | [(1,0), (3,2)] |
| (1,0) | (0,0) = 3 | [(3,2), (0,0)] |
| (3,2) | (3,3) = 4 | [(0,0), (3,3)] |
| (0,0), (3,3) | none | [] |
Final grid: [[3,-1,0,1],[2,2,1,-1],[1,-1,2,-1],[0,-1,3,4]].
Edge cases to test:
- No gates → nothing changes.
- Rooms walled off from every gate stay INF.
- Several gates at the same distance.
Complexity: O(R·C) time and space.
Medium 1926. Nearest Exit from Entrance in Maze Meta
Problem: A maze has '.' (open) and '+' (wall) cells. From entrance, find the fewest steps to any open cell on the border (an exit). The entrance itself doesn't count as an exit. Return -1 if there's none.
maze = [["+","+",".","+"],[".",".",".","+"],["+","+","+","."]], entrance = [1,2]1maze = [["+","+","+"],[".",".","."],["+","+","+"]], entrance = [1,0]2Constraints: maze.length == m · maze[i].length == n · 1 ≤ m, n ≤ 100 · maze[i][j] is either '.' or '+'. · entrance.length == 2 · 0 ≤ entrancerow < m · 0 ≤ entrancecol < n · entrance will always be an empty cell.
Hint 1
An exit is an empty border cell that isn't the entrance.
Hint 2
BFS from the entrance; the first exit you reach is the nearest.
From brute force to the pattern: DFS doesn't give the shortest distance without exploring everything. BFS finds the nearest exit in O(r · c).
Spot it: "fewest steps" on a grid is BFS. The target is any border cell, so test for it when you enqueue.
Approach:
- Mark the entrance as a wall (visited) and enqueue
(r, c, 0). - Pop a cell and look at its 4 open, in-bounds neighbours.
- If a neighbour is on the border, return
d + 1right away. - Otherwise mark it and enqueue it with
d + 1. If the queue empties, return -1.
class Solution:
def nearestExit(self, maze: List[List[str]], entrance: List[int]) -> int:
rows = len(maze)
cols = len(maze[0])
start_row = entrance[0]
start_col = entrance[1]
# Turn the entrance into a wall: it is visited, and it can't count as an exit.
maze[start_row][start_col] = "+"
# The queue holds (row, col, steps taken so far).
# append(x) adds at the back, popleft() takes from the front.
queue = deque()
queue.append((start_row, start_col, 0))
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
while len(queue) > 0:
item = queue.popleft()
row = item[0]
col = item[1]
steps = item[2]
for direction in directions:
new_row = row + direction[0]
new_col = col + direction[1]
if new_row < 0 or new_row >= rows or new_col < 0 or new_col >= cols:
continue
if maze[new_row][new_col] != ".":
continue # a wall, or already visited
# An open cell on the border is an exit. The first one found is the nearest.
if new_row == 0 or new_row == rows - 1 or new_col == 0 or new_col == cols - 1:
return steps + 1
maze[new_row][new_col] = "+" # mark on enqueue
queue.append((new_row, new_col, steps + 1))
return -1
What the code is doing, line by line:
start_rowandstart_colare read fromentranceby indexing.- The entrance is overwritten with
"+". That marks it visited, and it also stops it from being counted as an exit later. - The queue holds
(row, col, steps), starting with 0 steps. - Each pop is split by indexing. Neighbours outside the grid, walls, and visited cells are skipped with
continue. - If an open neighbour lies on the border (first/last row or column), it's an exit. BFS reaches cells in order of distance, so return
steps + 1right away. - Otherwise mark it and queue it with
steps + 1. An empty queue means there's no exit: return -1.
Dry run on maze = [["+","+",".","+"],[".",".",".","+"],["+","+","+","."]], entrance = [1,2] (answer 1):
| Pop (row, col, steps) | Neighbour checked | Result |
|---|---|---|
| (1, 2, 0) | down (2,2) = "+" | wall, skip |
| up (0,2) = "." | row 0 is the border → return 0 + 1 = 1 |
Edge cases to test:
- The entrance is on the border (it doesn't count as an exit).
- No reachable exit → −1.
- A 1 × 1 maze → −1.
Complexity: O(R·C) time and space.
Hard 847. Shortest Path Visiting All Nodes Google Microsoft
Problem: In a connected undirected graph, return the length of the shortest walk that visits every node. You may start and end anywhere, and revisit nodes and edges.
graph = [[1,2,3],[0],[0],[0]]4graph = [[1],[0,2,4],[1,3,4],[2],[1,2]]4Constraints: n == graph.length · 1 ≤ n ≤ 12 · 0 ≤ graph[i].length < n · graph[i] does not contain i. · If graph[a] contains b, then graph[b] contains a. · The input graph is always connected.
Hint 1
A state must include which nodes have been visited, not just the current node: (node, visitedMask).
Hint 2
BFS over (node, mask) states starting from every node at once; stop when the mask contains all nodes.
From brute force to the pattern: Trying every order of visiting nodes is O(n!). BFS over the n · 2ⁿ states finds the shortest walk; that's fine for n ≤ 12.
Spot it: "shortest" with unit edges means BFS, but the position alone isn't enough state. The real state is (node, set of nodes visited so far), stored as a bitmask. Since you can start anywhere, it's multi-source.
Approach:
- The goal mask is
full = (1 << n) - 1, with every bit set. - Seed the queue with
(u, 1 << u, 0)for every nodeu(multi-source). - Pop a state. If its mask is
full, return its distance. - For each neighbour
v, the new state is(v, mask | 1 << v). Enqueue it if that state hasn't been seen.
class Solution:
def shortestPathLength(self, graph: List[List[int]]) -> int:
node_count = len(graph)
# A bitmask stores a set of nodes in one integer: bit u is 1 if node u is visited.
# full has every bit set, which means "all nodes visited".
full = (1 << node_count) - 1
# The queue holds (node, mask, steps). seen holds (node, mask) states.
# append(x) adds at the back, popleft() takes from the front.
queue = deque()
seen = set()
# We may start anywhere: every node is a source that has visited only itself.
for node in range(node_count):
start_mask = 1 << node
queue.append((node, start_mask, 0))
seen.add((node, start_mask))
while len(queue) > 0:
item = queue.popleft()
node = item[0]
mask = item[1]
steps = item[2]
if mask == full:
return steps
for neighbour in graph[node]:
# Walk to neighbour and add it to the visited set (bitwise OR sets its bit).
new_mask = mask | (1 << neighbour)
state = (neighbour, new_mask)
if state not in seen: # same node + same visited set = same future
seen.add(state)
queue.append((neighbour, new_mask, steps + 1))
return 0
What the code is doing, line by line:
- A bitmask is an integer used as a set: bit
uis 1 when nodeuhas been visited.1 << uis the number with only bituset.full = (1 << node_count) - 1has all bits set (for 4 nodes, binary 1111 = 15). - The seeding loop puts every node in the queue as
(node, only-itself mask, 0 steps), because we may start anywhere (multi-source BFS). The same(node, mask)pairs go intoseen. - Each pop is split into
node,mask,steps. If the mask is full, every node is visited: returnsteps. - Moving to a neighbour gives
new_mask = mask | (1 << neighbour)(the|turns the neighbour's bit on). The state(neighbour, new_mask)is queued only if it's new: standing on the same node having visited the same set means the future is identical.
Dry run on the star graph [[1,2,3],[0],[0],[0]] (answer 4). Masks are shown in decimal (full = 15):
| steps | States popped at this level (node, mask) |
|---|---|
| 0 | (0,1), (1,2), (2,4), (3,8) |
| 1 | (1,3), (2,5), (3,9), (0,3), (0,5), (0,9) |
| 2 | (2,7), (3,11), (1,7), (3,13), (1,11), (2,13) |
| 3 | (0,7), (0,11), (0,13) |
| 4 | (3,15) → mask is full → return 4 (e.g. walk 1 → 0 → 2 → 0 → 3) |
Edge cases to test:
- A single node → 0.
- Revisiting nodes is allowed (and sometimes needed).
- A path graph vs a star graph.
Complexity: O(2n · n²) time and O(2n · n) space. That's fine because n ≤ 12.
Hard 1293. Shortest Path in a Grid with Obstacles Elimination Amazon Cisco
Problem: Move up, down, left or right through a 0/1 grid from the top-left to the bottom-right. You may remove at most k obstacles (1s). Return the minimum number of steps, or -1.
grid = [[0,0,0],[1,1,0],[0,0,0],[0,1,1],[0,0,0]], k = 16grid = [[0,1,1],[1,1,1],[1,0,0]], k = 1-1Constraints: m == grid.length · n == grid[i].length · 1 ≤ m, n ≤ 40 · 1 ≤ k ≤ m * n · grid[i][j] is either 0 or 1. · grid[0][0] == grid[m - 1][n - 1] == 0
Hint 1
The state needs the number of obstacles you can still remove: (row, col, k_left).
Hint 2
BFS over those states; a cell may be revisited if you arrive with more removals left than before.
From brute force to the pattern: Plain BFS ignores removals; DFS over every choice is exponential. BFS over (cell, removals left) states finds the shortest path in O(r · c · k).
Spot it: "minimum steps" means BFS, but the same cell reached with more removals left is a better state. So the state is (r, c, removals_left).
Approach:
- Shortcut: if
k ≥ R + C - 2, you can walk straight through anything, so returnR + C - 2. - BFS over
(r, c, left, steps), starting at(0, 0, k, 0). - Pop a state. If it's the bottom-right cell, return
steps. - For each in-bounds neighbour,
nl = left - grid[nr][nc](stepping on an obstacle uses one removal). Ifnl ≥ 0and the state is unseen, enqueue it.
class Solution:
def shortestPath(self, grid: List[List[int]], k: int) -> int:
rows = len(grid)
cols = len(grid[0])
# A straight walk takes rows + cols - 2 steps. With that many removals,
# nothing can block us, so that is the answer.
if k >= rows + cols - 2:
return rows + cols - 2
# The queue holds (row, col, removals left, steps).
# append(x) adds at the back, popleft() takes from the front.
queue = deque()
queue.append((0, 0, k, 0))
# seen holds (row, col, removals left). The same cell with more removals
# left is a different (better) state, so it gets its own entry.
seen = set()
seen.add((0, 0, k))
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
while len(queue) > 0:
item = queue.popleft()
row = item[0]
col = item[1]
left = item[2]
steps = item[3]
if row == rows - 1 and col == cols - 1:
return steps
for direction in directions:
new_row = row + direction[0]
new_col = col + direction[1]
if new_row < 0 or new_row >= rows or new_col < 0 or new_col >= cols:
continue
# An obstacle (1) uses up one removal; an open cell (0) costs nothing.
new_left = left - grid[new_row][new_col]
if new_left >= 0 and (new_row, new_col, new_left) not in seen:
seen.add((new_row, new_col, new_left))
queue.append((new_row, new_col, new_left, steps + 1))
return -1
What the code is doing, line by line:
- The shortest possible walk is
rows + cols - 2steps (only down and right). Ifkis at least that, we can remove anything in the way, so return it. - The queue holds
(row, col, left, steps), whereleftis how many removals we still have.seenstores(row, col, left): arriving at the same cell with a different number of removals left is a different state. - Each pop is split into four variables by indexing. Reaching the bottom-right cell returns
steps. - For each neighbour inside the grid,
new_left = left - grid[new_row][new_col]: a 1 (obstacle) costs one removal, a 0 costs nothing. - If
new_leftis still 0 or more and that state is new, mark it and queue it withsteps + 1. An empty queue means we can't get through: return -1.
Dry run on grid = [[0,0,0],[1,1,0],[0,0,0],[0,1,1],[0,0,0]], k = 1 (answer 6). States are (row, col, left), grouped by BFS level:
| steps | States popped at this level |
|---|---|
| 0 | (0,0,1) |
| 1 | (1,0,0) (removed an obstacle), (0,1,1) |
| 2 | (2,0,0), (0,0,0), (1,1,0), (0,2,1) |
| 3 | (3,0,0), (2,1,0), (0,1,0), (1,2,0), (1,2,1) |
| 4 | (4,0,0), (2,2,0), (0,2,0), (2,2,1) |
| 5 | (4,1,0), (3,2,0), (2,1,1) |
| 6 | (4,2,0) → bottom-right → return 6 |
The winning path goes down the left side, removing the obstacle at (1,0), then along the bottom row.
Edge cases to test:
k≥r + c − 3→ the Manhattan distance (you can go straight through).- A 1 × 1 grid → 0.
- Unreachable even with removals → −1.
Complexity: O(R·C·k) time and space.
Easy 637. Average of Levels in Binary Tree Start here
Problem: Return the average value of the nodes on each level of the tree, from top to bottom.
Solve it on LeetCode ↗ · Video walkthrough ↗
root = [3,9,20,null,null,15,7][3.00000,14.50000,11.00000]root = [3,9,20,15,7][3.00000,14.50000,11.00000]Constraints: The number of nodes in the tree is in the range [1, 10⁴]. · -2³¹ ≤ Node.val ≤ 2³¹ - 1
Hint 1
Process the tree one level at a time.
Hint 2
BFS: sum the values of each level and divide by the level's size.
From brute force to the pattern: DFS collecting (sum, count) per depth also works in O(n). BFS gives the levels directly in order.
Spot it: "Per level" → BFS with a queue, processing exactly one level per round (len(queue) nodes).
Approach:
- Put the root in a queue.
- While the queue isn't empty: the current level is the
len(queue)nodes in it right now. - Pop each of them, add its value to a running total, and push its children (they form the next level).
- Append
total / level_size.
from collections import deque
class Solution:
def averageOfLevels(self, root: Optional[TreeNode]) -> List[float]:
averages = []
queue = deque([root])
while queue:
level_size = len(queue) # nodes on the current level
total = 0
for _ in range(level_size):
node = queue.popleft()
total = total + node.val
# children belong to the next level
if node.left is not None:
queue.append(node.left)
if node.right is not None:
queue.append(node.right)
averages.append(total / level_size)
return averages
What the code is doing, line by line:
deque([root]): a double-ended queue;popleft()isO(1)(a list'spop(0)isO(n)).level_size = len(queue): measured before the loop, so children pushed during it wait for the next round.for _ in range(level_size):pop exactly the nodes of this level and add their values.queue.append(node.left)/node.right: enqueue the next level.total / level_size:/is true division, so29 / 2gives14.5.
Dry run on the example:
root = [3, 9, 20, null, null, 15, 7] (LeetCode Example 1). Returns [3.0, 14.5, 11.0].
| level | nodes popped | total | average | queue after |
|---|---|---|---|---|
| 0 | 3 | 3 | 3.0 | [9, 20] |
| 1 | 9, 20 | 29 | 14.5 | [15, 7] |
| 2 | 15, 7 | 22 | 11.0 | [] |
Edge cases to test:
- A single node.
- Large values (Python's integers don't overflow).
- Uneven levels.
Complexity: O(n) time, O(w) space for the queue (w = the widest level).
Medium 117. Populating Next Right Pointers in Each Node II Amazon Microsoft
Problem: Each node has an extra next pointer. Point every node's next at the node directly to its right on the same level, or None if it is the last one. The tree is not necessarily perfect.
Solve it on LeetCode ↗ · Video walkthrough ↗
root = [1,2,3,4,5,null,7][1,#,2,3,#,4,5,7,#]root = [][]Constraints: The number of nodes in the tree is in the range [0, 6000]. · -100 ≤ Node.val ≤ 100
Hint 1
Nodes on the same level should point to their right neighbour.
Hint 2
BFS level by level and link each node to the next node popped on that level. (Follow-up: use the next pointers you already built to link the level below in O(1) space.)
From brute force to the pattern: Collecting each level into a list and linking it is O(n) time and space. BFS links as it goes; the follow-up technique walks each level using its own next pointers with O(1) extra space.
Spot it: "Same level, left to right" is exactly the order BFS pops nodes. Within one level, each popped node's next is simply the following node in the queue.
Key insight: A follow-up asks for O(1) extra space: walk each level along the next pointers you already built while linking the level below with a dummy head. The BFS version below is the clearest starting point.
Approach:
- BFS with a queue, one level per round.
- Pop the level's nodes in order, remembering the previously popped node
prev. - Set
prev.next = nodefor every node after the first; the last node keepsnext = None. - Push each node's children for the next level.
from collections import deque
class Solution:
def connect(self, root: "Node") -> "Node":
if root is None:
return None
queue = deque([root])
while queue:
level_size = len(queue)
prev = None
for _ in range(level_size):
node = queue.popleft()
if prev is not None:
# the node popped before this one is its left neighbour
prev.next = node
prev = node
if node.left is not None:
queue.append(node.left)
if node.right is not None:
queue.append(node.right)
# the last node of the level keeps next = None
return root
What the code is doing, line by line:
if root is None: return None: nothing to connect.level_size = len(queue): the number of nodes on this level.prev = Noneat the start of every level, so the first node of a level is never linked from the previous level's last node.if prev is not None: prev.next = node: chain the level together in the order the queue gives us, which is left to right.- Children are appended left before right, which keeps the next level in left-to-right order too.
Dry run on the example:
root = [1, 2, 3, 4, 5, null, 7] (LeetCode Example 1). Result levels: 1 → None, 2 → 3 → None, 4 → 5 → 7 → None.
| level | popped in order | links made |
|---|---|---|
| 0 | 1 | (none; 1.next stays None) |
| 1 | 2, 3 | 2.next = 3 |
| 2 | 4, 5, 7 | 4.next = 5, 5.next = 7 |
Edge cases to test:
- An empty tree.
- Missing children in the middle of a level.
- The last node of each level points to
None.
Complexity: O(n) time, O(w) queue space (O(1) with the follow-up technique).
Medium 433. Minimum Genetic Mutation Google Amazon
Problem: A gene is an 8-character string of A, C, G, T. One mutation changes one character, and every intermediate gene must be in bank. Return the minimum number of mutations from startGene to endGene, or -1 if impossible.
Solve it on LeetCode ↗ · Video walkthrough ↗
startGene = "AACCGGTT", endGene = "AACCGGTA", bank = ["AACCGGTA"]1startGene = "AACCGGTT", endGene = "AAACGGTA", bank = ["AACCGGTA","AACCGCTA","AAACGGTA"]2Constraints: 0 ≤ bank.length ≤ 10 · startGene.length == endGene.length == bank[i].length == 8 · startGene, endGene, and bank[i] consist of only the characters ['A', 'C', 'G', 'T'].
Hint 1
Each gene is a node; one valid mutation is an edge.
Hint 2
BFS from startGene, trying all 8 × 3 mutations of each gene and keeping only bank genes.
From brute force to the pattern: DFS doesn't give the minimum number of mutations. BFS finds the shortest chain in O(b · 8 · 4).
Spot it: "Minimum number of single changes" between states → each gene is a node, each valid mutation an edge, and all edges cost 1. Shortest path in an unweighted graph = BFS.
Approach:
- Put
bankin a set forO(1)membership checks. IfendGeneisn't in it, return-1straight away. - BFS from
startGenewith a step count, plus avisitedset. - For each popped gene, try every position × every other letter (
8 × 3 = 24mutations). Queue the ones that are in the bank and not yet visited. - The first time
endGeneis popped, its step count is the minimum.
from collections import deque
class Solution:
def minMutation(self, startGene: str, endGene: str, bank: List[str]) -> int:
valid = set(bank)
if endGene not in valid:
return -1
queue = deque([(startGene, 0)])
visited = {startGene}
while queue:
gene, steps = queue.popleft()
if gene == endGene:
return steps
# every gene that differs from this one in exactly one position
for i in range(len(gene)):
for ch in "ACGT":
if ch == gene[i]:
continue
mutated = gene[:i] + ch + gene[i + 1:]
if mutated in valid and mutated not in visited:
visited.add(mutated)
queue.append((mutated, steps + 1))
return -1
What the code is doing, line by line:
valid = set(bank): membership tests inO(1). If the target isn't in the bank, it can never be reached.queue = deque([(startGene, 0)]): each entry carries its distance from the start.visited = {startGene}: mark genes when they are queued, so the same gene is never queued twice.if gene == endGene: return steps: BFS pops genes in order of distance, so the first time we pop the target is the shortest route.mutated = gene[:i] + ch + gene[i + 1:]: copy the gene with positionireplaced bych.return -1: the queue emptied without reaching the target.
Dry run on the example:
startGene = "AACCGGTT", endGene = "AAACGGTA", bank = ["AACCGGTA", "AACCGCTA", "AAACGGTA"] (LeetCode Example 2). Returns 2.
| popped (steps) | valid unvisited mutations queued | queue after |
|---|---|---|
AACCGGTT (0) | AACCGGTA (position 7: T→A) | [AACCGGTA (1)] |
AACCGGTA (1) | AAACGGTA (position 2: C→A), AACCGCTA (position 5: G→C) | [AAACGGTA (2), AACCGCTA (2)] |
AAACGGTA (2) | it is endGene → return 2 |
Edge cases to test:
endGenenot in the bank → −1.startGene == endGene→ 0.- An empty bank.
Complexity: O(b · L · 4) time, where b is the bank size and L = 8; O(b) space for the set, queue and visited.
Medium 909. Snakes and Ladders Cisco
Problem: An n × n board is numbered 1..n² starting at the bottom-left, alternating direction on every row (boustrophedon). Each move, roll a die and go 1–6 squares forward; if you land on a snake or ladder (board[r][c] != -1), you must take it to that square. Return the fewest moves to reach n², or -1.
Solve it on LeetCode ↗ · Video walkthrough ↗
board = [[-1,-1,-1,-1,-1,-1],[-1,-1,-1,-1,-1,-1],[-1,-1,-1,-1,-1,-1],[-1,35,-1,-1,13,-1],[-1,-1,-1,-1,-1,-1],[-1,15,-1,-1,-1,-1]]4board = [[-1,-1],[-1,3]]1Constraints: n == board.length == board[i].length · 2 ≤ n ≤ 20 · board[i][j] is either -1 or in the range [1, n²]. · The squares labeled 1 and n² are not the starting points of any snake or ladder.
Hint 1
Each square is a node; a die roll moves you 1–6 squares, then any snake or ladder is mandatory.
Hint 2
BFS from square 1; the tricky part is converting a square number to (row, col) on the zig-zag board.
From brute force to the pattern: Trying every sequence of rolls is exponential. BFS over the n² squares finds the fewest moves in O(n²).
Spot it: "Fewest moves" where every move costs 1 → BFS over squares. Each square has up to six neighbours (s + 1 .. s + 6, then follow a snake/ladder).
Key insight: Convert a square number to its cell: row_from_bottom = (s − 1) // n, col = (s − 1) % n, and flip col on odd rows from the bottom because those rows run right-to-left. The real row index is n − 1 − row_from_bottom.
Approach:
- Write
cell(s)to turn a square number into(row, col). - BFS from square 1 with move count 0; mark squares as visited when queued.
- For each popped square, try
s + 1 .. min(s + 6, n²). If that cell has a snake or ladder, jump to its destination. - Return the move count when
n²is reached;-1if the queue empties.
from collections import deque
class Solution:
def snakesAndLadders(self, board: List[List[int]]) -> int:
n = len(board)
def cell(square):
# squares count from the bottom-left, zig-zagging every row
row_from_bottom = (square - 1) // n
col = (square - 1) % n
if row_from_bottom % 2 == 1:
col = n - 1 - col # odd rows (from the bottom) run right-to-left
return n - 1 - row_from_bottom, col
target = n * n
queue = deque([(1, 0)]) # (square, moves taken)
visited = {1}
while queue:
square, moves = queue.popleft()
if square == target:
return moves
for step in range(1, 7):
nxt = square + step
if nxt > target:
break
r, c = cell(nxt)
if board[r][c] != -1:
nxt = board[r][c] # forced to take the snake or ladder
if nxt not in visited:
visited.add(nxt)
queue.append((nxt, moves + 1))
return -1
What the code is doing, line by line:
def cell(square):row0ofboardis the top, but square 1 is at the bottom, hencen - 1 - row_from_bottom.if row_from_bottom % 2 == 1: col = n - 1 - col: rows alternate direction, so every second row is mirrored.queue = deque([(1, 0)]): start on square 1 having made 0 moves.for step in range(1, 7):the six die faces.if nxt > target: break: you can't move past the last square.if board[r][c] != -1: nxt = board[r][c]: a snake or ladder is mandatory, so the neighbour is its destination.if nxt not in visited:BFS reaches each square first by the fewest moves, so later visits are useless.
Dry run on the example:
LeetCode Example 1, a 6 × 6 board with a ladder 2 → 15, a ladder 14 → 35 and a snake 17 → 13. Returns 4 (1 → 2↑15 → 17↓13 → 14↑35 → 36). Each row is one BFS level.
| moves | squares first reached with this many moves |
|---|---|
| 0 | 1 |
| 1 | 15, 3, 4, 5, 6, 7 |
| 2 | 16, 13, 18, 19, 20, 21, 8, 9, 10, 11, 12 |
| 3 | 22, 35, 23, 24, 25, 26, 27 |
| 4 | 28, 36, 29, 30, 31, 32, 33 → 36 reached |
Rolling a 2 from square 1 lands on the ladder, which is why 15 is reached in one move and 2 never appears. Likewise 17 is a snake down to 13, and 14 is a ladder up to 35, which is then one roll from 36.
Edge cases to test:
- A ladder or snake on square 1 is never taken (you start there).
- You can't chain a snake/ladder into another in the same move.
- The target unreachable → −1.
Complexity: O(n²) time (each square is queued at most once, with 6 tries), O(n²) space.
Medium 863. All Nodes Distance K in Binary Tree Google Meta Amazon Microsoft Apple
Problem: Return the values of all nodes at distance exactly k from the target node (any order).
root = [3,5,1,6,2,0,8,null,null,7,4], target = 5, k = 2[7,4,1]root = [1], target = 1, k = 3[]Constraints: The number of nodes in the tree is in the range [1, 500]. · 0 ≤ Node.val ≤ 500 · All the values Node.val are unique. · target is the value of one of the nodes in the tree. · 0 ≤ k ≤ 1000
Hint 1
The path to a node at distance k may go up through the target's ancestors.
Hint 2
Record each node's parent, then BFS from the target.
From brute force to the pattern: Computing the distance from target to every node via LCA separately is O(n · h); one BFS on the parent-linked tree is O(n).
Spot it: Distance can go up through parents as well as down. Record each node's parent (one DFS), then treat the tree as an undirected graph and run BFS from target for k layers.
Why not another pattern? Only searching down from target misses nodes reached through its ancestors.
Approach:
- DFS from the root to fill
parent[node]. - BFS from
targetwith a visited set, using left, right and parent as neighbours. - After
klayers, the queue holds exactly the answer.
from collections import deque
class Solution:
def distanceK(self, root: TreeNode, target: TreeNode, k: int) -> List[int]:
parent = {root: None}
stack = [root]
while stack:
node = stack.pop()
for child in (node.left, node.right):
if child:
parent[child] = node # lets BFS walk upward too
stack.append(child)
queue = deque([target])
seen = {target}
for _ in range(k): # expand exactly k layers
for _ in range(len(queue)):
node = queue.popleft()
for nb in (node.left, node.right, parent[node]):
if nb and nb not in seen:
seen.add(nb)
queue.append(nb)
return [node.val for node in queue] # everything exactly k away
What the code is doing, line by line:
- The parent map turns each node's neighbours into left, right and parent.
- BFS layer
dholds exactly the nodes at distancedfromtarget. seenstops the search from walking back the way it came.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
k = 0→[target.val].klarger than the tree →[].- Target is the root.
Complexity: O(n) time and space.
Medium 934. Shortest Bridge Google
Problem: A binary grid has exactly two islands. Return the fewest 0s to flip to connect them.
grid = [[0,1],[1,0]]1grid = [[0,1,0],[0,0,0],[0,0,1]]2Constraints: n == grid.length == grid[i].length · 2 ≤ n ≤ 100 · grid[i][j] is either 0 or 1. · There are exactly two islands in grid.
Hint 1
First find all cells of one island.
Hint 2
Then BFS outward from all of them at once; count layers until you hit the other island.
From brute force to the pattern: BFS from each cell of the first island separately is O(n⁴); multi-source BFS is O(n²).
Spot it: The answer is the shortest water distance between the islands. Find all cells of one island (DFS), then expand from all of them at once (multi-source BFS); the layer at which another 1 is touched is the answer.
Why not another pattern? BFS from every cell of island one separately is O(n⁴).
Approach:
- Find any 1; flood-fill its island, marking cells as 2 and queueing them.
- BFS layer by layer over 0s, marking visited water as 2.
- When a neighbour is a 1 (the other island), return the current layer count.
from collections import deque
class Solution:
def shortestBridge(self, grid: List[List[int]]) -> int:
n = len(grid)
start = next((r, c) for r in range(n) for c in range(n) if grid[r][c] == 1)
queue = deque()
stack = [start]
grid[start[0]][start[1]] = 2
while stack: # mark the first island
r, c = stack.pop()
queue.append((r, c))
for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
if 0 <= nr < n and 0 <= nc < n and grid[nr][nc] == 1:
grid[nr][nc] = 2
stack.append((nr, nc))
flips = 0
while queue: # grow outward from the whole island at once
for _ in range(len(queue)):
r, c = queue.popleft()
for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
if 0 <= nr < n and 0 <= nc < n:
if grid[nr][nc] == 1:
return flips # touched the second island
if grid[nr][nc] == 0:
grid[nr][nc] = 2 # water reached: one more flip
queue.append((nr, nc))
flips += 1
return -1
What the code is doing, line by line:
- Marking the first island as 2 separates it from the second (still 1).
- All of the first island starts the BFS together, so layer
dholds the water cells at distanced. - Touching a 1 while expanding layer
flipsmeansflipswater cells lie between the islands.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Islands touching diagonally (still need 1 flip).
- Islands one cell apart.
- Large islands far apart.
Complexity: O(n²) time and space.
Medium 1129. Shortest Path with Alternating Colors Google
Problem: In a directed graph with red and blue edges, return for each node the shortest path length from 0 whose edge colours alternate (−1 if none).
n = 3, redEdges = [[0,1],[1,2]], blueEdges = [][0,1,-1]n = 3, redEdges = [[0,1]], blueEdges = [[2,1]][0,1,-1]Constraints: 1 ≤ n ≤ 100 · 0 ≤ redEdges.length, blueEdges.length ≤ 400 · redEdges[i].length == blueEdges[j].length == 2 · 0 ≤ ai, bi, uj, vj < n
Hint 1
What else, besides the node, decides where you can go next?
Hint 2
BFS over (node, colour of last edge).
From brute force to the pattern: Enumerating alternating paths with DFS is exponential; BFS over 2n states is linear.
Spot it: Which edges you may take next depends on the colour you just used, so the BFS state is (node, last colour); each node can be reached twice.
Why not another pattern? A BFS on nodes alone marks a node visited after one colour and wrongly blocks the other colour's route.
Approach:
- Adjacency lists per colour.
- BFS from
(0, red)and(0, blue)at distance 0. - From
(u, colour)follow edges of the other colour to(v, other); the first time a node is reached, record its distance.
from collections import deque
class Solution:
def shortestAlternatingPaths(self, n: int, redEdges: List[List[int]], blueEdges: List[List[int]]) -> List[int]:
adj = [[[] for _ in range(n)] for _ in range(2)] # adj[0] red, adj[1] blue
for a, b in redEdges:
adj[0][a].append(b)
for a, b in blueEdges:
adj[1][a].append(b)
answer = [-1] * n
answer[0] = 0
queue = deque([(0, 0, 0), (0, 1, 0)]) # node, colour of the last edge, distance
seen = {(0, 0), (0, 1)}
while queue:
u, colour, d = queue.popleft()
nxt_colour = 1 - colour # must switch colour
for v in adj[nxt_colour][u]:
if (v, nxt_colour) not in seen:
seen.add((v, nxt_colour))
if answer[v] == -1:
answer[v] = d + 1 # first arrival is the shortest
queue.append((v, nxt_colour, d + 1))
return answer
What the code is doing, line by line:
- Starting with both colours at node 0 lets the first edge be either red or blue.
- Each state follows only edges of the opposite colour.
- BFS reaches states in increasing distance, so the first arrival at a node (in either colour) is its answer.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Self-loops and parallel edges of both colours.
- Unreachable nodes → −1.
- Node 0 → 0.
Complexity: O(n + e) time and space.
Medium 3629. Minimum Jumps to Reach End via Prime Teleportation Google Amazon
Problem: From index i you may step to i ± 1, or, if nums[i] is a prime p, teleport to any j with nums[j] % p == 0. Return the minimum jumps from 0 to n − 1.
nums = [1,2,4,6]2nums = [2,3,4,7,9]2Constraints: 1 ≤ n == nums.length ≤ 10⁵ · 1 ≤ nums[i] ≤ 10⁶
Hint 1
Minimum number of jumps: BFS.
Hint 2
For each prime, precompute the indices whose value it divides.
Hint 3
As in Jump Game IV, use each prime's teleport list only once.
From brute force to the pattern: Scanning all indices for each prime you land on is O(n²). Grouping by prime factor and clearing groups keeps BFS near-linear.
Spot it: Fewest jumps → BFS. Teleport edges are dense, like Jump Game IV: group indices by each prime factor of their value (sieve of smallest prime factors), and after a prime's group is used once, clear it.
Why not another pattern? Checking nums[j] % p for every j each time a prime is reached is O(n²).
Approach:
- Sieve the smallest prime factor up to
max(nums). - For each index, add it to
groups[p]for every distinct primepdividingnums[j]. - BFS from 0 with neighbours
i ± 1, plusgroups[nums[i]]whennums[i]is prime; clear that group after use.
from collections import defaultdict, deque
class Solution:
def minJumps(self, nums: List[int]) -> int:
n = len(nums)
top = max(nums)
spf = list(range(top + 1)) # smallest prime factor of each number
for p in range(2, int(top ** 0.5) + 1):
if spf[p] == p:
for m in range(p * p, top + 1, p):
if spf[m] == m:
spf[m] = p
groups = defaultdict(list) # prime -> indices whose value it divides
for j, x in enumerate(nums):
while x > 1:
p = spf[x]
groups[p].append(j)
while x % p == 0:
x //= p
dist = [-1] * n
dist[0] = 0
queue = deque([0])
while queue:
i = queue.popleft()
if i == n - 1:
return dist[i]
targets = [i - 1, i + 1]
x = nums[i]
if x > 1 and spf[x] == x: # nums[i] is prime: teleport
targets += groups[x]
groups[x] = [] # these indices are queued now; never scan them again
for j in targets:
if 0 <= j < n and dist[j] == -1:
dist[j] = dist[i] + 1
queue.append(j)
return -1
What the code is doing, line by line:
- The sieve gives each number's smallest prime factor, which lets us factor every value quickly.
groups[p]lists exactly the indices a primepcan teleport to.- After one teleport from any index with value
p, all those targets are queued, so the group is emptied. - BFS distances are the minimum jump counts.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
n = 1→ 0.- Value 1 (not prime, divisible by nothing).
- Many copies of the same prime.
Complexity: O(M log log M + n log M) time for the sieve and factoring (M = max(nums)), O(M + n log M) space.
Hard 126. Word Ladder II Google Amazon Microsoft
Problem: Return all shortest transformation sequences from beginWord to endWord, changing one letter at a time through words of wordList.
beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"][["hit","hot","dot","dog","cog"],["hit","hot","lot","log","cog"]]beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"][]Constraints: 1 ≤ beginWord.length ≤ 5 · endWord.length == beginWord.length · 1 ≤ wordList.length ≤ 500 · wordList[i].length == beginWord.length · beginWord, endWord, and wordList[i] consist of lowercase English letters. · beginWord != endWord · All the words in wordList are unique. · The sum of all shortest transformation sequences does not exceed 10⁵.
Hint 1
Find the shortest distance first with BFS.
Hint 2
While doing BFS, remember all parents of each word that lie on a shortest path.
Hint 3
Rebuild every ladder by backtracking from the end word.
From brute force to the pattern: DFS over every transformation sequence is exponential. BFS with parent sets keeps only shortest-path edges.
Spot it: Shortest → BFS. All shortest paths → during BFS, record for each word every word on the previous layer that leads to it (its parents), and only expand words not seen in earlier layers. Then backtrack from endWord through the parents.
Why not another pattern? DFS over all paths explores exponentially many long paths; storing full paths in the BFS queue blows up memory.
Approach:
- BFS from
beginWordone layer at a time; remove the whole layer from the word set before expanding it. - For each word, try all one-letter changes; any that is in the set gets this word added to its
parents. - Stop after the layer that reaches
endWord. - Backtrack from
endWordtobeginWordthroughparents, building each path in reverse.
from collections import defaultdict
class Solution:
def findLadders(self, beginWord: str, endWord: str, wordList: List[str]) -> List[List[str]]:
words = set(wordList)
if endWord not in words:
return []
parents = defaultdict(set) # word -> words one layer earlier that lead to it
layer = {beginWord}
words.discard(beginWord)
found = False
while layer and not found:
nxt = defaultdict(set)
for word in layer:
for i in range(len(word)):
for ch in "abcdefghijklmnopqrstuvwxyz":
cand = word[:i] + ch + word[i + 1:]
if cand in words:
nxt[cand].add(word) # one more shortest way into cand
if cand == endWord:
found = True
words -= nxt.keys() # words reached now can't appear on a later layer
for w, ps in nxt.items():
parents[w] |= ps
layer = set(nxt)
if not found:
return []
ladders = []
path = [endWord]
def back(word):
if word == beginWord:
ladders.append(path[::-1]) # reached the start: one full ladder
return
for p in parents[word]:
path.append(p)
back(p)
path.pop()
back(endWord)
return ladders
What the code is doing, line by line:
- Words are removed from the set only after a whole layer is processed, so several words of the same layer can all become parents of one word.
- A word reached in an earlier layer is never used again, which keeps every recorded edge on a shortest path.
- Backtracking from
endWordfollows only shortest-path edges, so every path found has minimum length.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
endWordnot in the list →[].- Several shortest ladders sharing words.
beginWordone step fromendWord.
Complexity: O(N · L · 26) for the BFS (N words of length L) plus the size of the output for backtracking.
Hard 864. Shortest Path to Get All Keys Google
Problem: In a grid with walls, keys a–f and locks A–F, return the fewest moves from @ to collect every key, or −1.
grid = ["@.a..","###.#","b.A.B"]8grid = ["@..aA","..B#.","....b"]6Constraints: m == grid.length · n == grid[i].length · 1 ≤ m, n ≤ 30 · grid[i][j] is either an English letter, '.', '#', or '@'. · There is exactly one '@' in the grid. · The number of keys in the grid is in the range [1, 6]. · Each key in the grid is unique. · Each key in the grid has a matching lock.
Hint 1
BFS on cells is not enough. What else describes where you are in the search?
Hint 2
Add the set of collected keys (as a bitmask) to the BFS state.
From brute force to the pattern: Trying every key order (K!) with a BFS for each leg is slower and trickier; BFS over (cell, mask) handles it in one search.
Spot it: Shortest moves on a grid → BFS, but the position alone isn't the state: what you can do depends on the keys held. Use state (r, c, mask) where mask has a bit per key (at most 2⁶ = 64 masks).
Why not another pattern? BFS on cells alone forbids revisiting a cell after picking up a key, which is often necessary.
Approach:
- Find
@and the number of keys;goal = (1 << keys) − 1. - BFS from
(start, mask 0). Skip walls and locks whose key bit isn't set. - Stepping on a key sets its bit. Return the distance when
mask == goal.
from collections import deque
class Solution:
def shortestPathAllKeys(self, grid: List[str]) -> int:
rows, cols = len(grid), len(grid[0])
keys = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == "@":
start = (r, c)
elif grid[r][c].islower():
keys += 1
goal = (1 << keys) - 1 # every key bit set
queue = deque([(start[0], start[1], 0, 0)]) # row, col, keys held, moves
seen = {(start[0], start[1], 0)}
while queue:
r, c, mask, moves = queue.popleft()
for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
if not (0 <= nr < rows and 0 <= nc < cols) or grid[nr][nc] == "#":
continue
cell = grid[nr][nc]
if cell.isupper() and not mask >> (ord(cell) - ord("A")) & 1:
continue # locked and we don't have the key
nmask = mask
if cell.islower():
nmask = mask | 1 << (ord(cell) - ord("a")) # pick up the key
if nmask == goal:
return moves + 1
if (nr, nc, nmask) not in seen:
seen.add((nr, nc, nmask))
queue.append((nr, nc, nmask, moves + 1))
return -1
What the code is doing, line by line:
- The state includes the keys held, so revisiting a cell with more keys is allowed and useful.
- A lock is passable only when its key's bit is set in the mask.
- BFS reaches every state in the fewest moves, so the first time the mask is complete is optimal.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- A lock blocks the only path to its own key → −1.
- Keys collected in a non-obvious order.
- A single key next to the start.
Complexity: O(m · n · 2ᴷ) time and space (K ≤ 6 keys).
Hard 1345. Jump Game IV Google Amazon
Problem: From index i you can move to i − 1, i + 1, or any j with arr[j] == arr[i]. Return the fewest steps from index 0 to the last index.
arr = [100,-23,-23,404,100,23,23,23,3,404]3arr = [7]0Constraints: 1 ≤ arr.length ≤ 5 * 10⁴ · -10⁸ ≤ arr[i] ≤ 10⁸
Hint 1
This is a shortest path in an unweighted graph: BFS.
Hint 2
Equal values create many edges. After you've used a value's teleport once, do you ever need it again?
From brute force to the pattern: Scanning each value group from every index is O(n²) for repeated values. Clearing groups after first use makes BFS O(n).
Spot it: Fewest steps on an unweighted graph → BFS. The catch: equal values form cliques with up to n² edges. Once a value's group has been expanded, every member is already queued, so delete the group to never scan it again.
Why not another pattern? Plain BFS rescanning each value group from every member is O(n²) on arrays like [7,7,7,…].
Approach:
- Map each value to its list of indices.
- BFS from 0; neighbours:
i − 1,i + 1, and every index ingroups[arr[i]]. - After using
groups[arr[i]], clear it. - Return the layer when the last index is reached.
from collections import defaultdict, deque
class Solution:
def minJumps(self, arr: List[int]) -> int:
n = len(arr)
groups = defaultdict(list)
for i, x in enumerate(arr):
groups[x].append(i)
seen = [False] * n
seen[0] = True
queue = deque([0])
steps = 0
while queue:
for _ in range(len(queue)):
i = queue.popleft()
if i == n - 1:
return steps
for j in groups[arr[i]] + [i - 1, i + 1]:
if 0 <= j < n and not seen[j]:
seen[j] = True
queue.append(j)
groups[arr[i]] = [] # every equal-valued index is queued now: never scan them again
steps += 1
return -1
What the code is doing, line by line:
- The value groups are the teleport edges.
- Clearing a group after one use is what keeps the BFS linear: each group is scanned once in total.
- BFS layers count steps, so reaching
n − 1returns the minimum.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
n = 1→ 0.- All values equal → 1 step.
- No repeated values (only ±1 moves).
Complexity: O(n) time and space.
Hard 2045. Second Minimum Time to Reach Destination Google
Problem: Every edge takes time minutes; all signals toggle green/red every change minutes and you may leave only on green. Return the second smallest (strictly larger) arrival time from 1 to n.
n = 5, edges = [[1,2],[1,3],[1,4],[3,4],[4,5]], time = 3, change = 513n = 2, edges = [[1,2]], time = 3, change = 211Constraints: 2 ≤ n ≤ 10⁴ · n - 1 ≤ edges.length ≤ min(2 * 10⁴, n * (n - 1) / 2) · edges[i].length == 2 · 1 ≤ ui, vi ≤ n · ui != vi · There are no duplicate edges. · Each vertex can be reached directly or indirectly from every other vertex. · 1 ≤ time, change ≤ 10³
Hint 1
Does the arrival time depend on anything except the number of edges walked?
Hint 2
Find the second smallest number of edges from 1 to n: BFS that allows two visits per node.
Hint 3
Then simulate the traffic lights along that many edges.
From brute force to the pattern: Enumerating paths is exponential; Dijkstra over (node, rank) works but a two-visit BFS is simpler and linear.
Spot it: Signals are shared, so total time depends only on the number of edges walked: fewer edges is always faster. So find the second smallest distinct path length (in edges) with a BFS that lets each node be reached twice (first and second distinct distance), then simulate the signals along that many edges.
Why not another pattern? Dijkstra on times works but is overkill; time is monotone in edge count, so BFS on counts is enough.
Approach:
- BFS storing
first[v]andsecond[v](distinct edge counts). - When reaching
vwith distanced: iffirstis unset, set it; elifd > first[v]andsecondis unset, setsecond. Enqueue only on such updates. - Simulate
second[n]edges: before each edge, if the light is red ((t // change) % 2 == 1), wait until it turns green; then addtime.
from collections import deque
class Solution:
def secondMinimum(self, n: int, edges: List[List[int]], time: int, change: int) -> int:
adj = [[] for _ in range(n + 1)]
for a, b in edges:
adj[a].append(b)
adj[b].append(a)
first = [-1] * (n + 1) # shortest edge count to each node
second = [-1] * (n + 1) # second shortest (strictly larger)
first[1] = 0
queue = deque([(1, 0)])
while queue:
u, d = queue.popleft()
for v in adj[u]:
if first[v] == -1:
first[v] = d + 1
queue.append((v, d + 1))
elif second[v] == -1 and d + 1 > first[v]:
second[v] = d + 1 # a strictly longer way in
queue.append((v, d + 1))
t = 0
for _ in range(second[n]): # walk that many edges
if (t // change) % 2 == 1:
t = (t // change + 1) * change # red: wait for green
t += time
return t
What the code is doing, line by line:
- Everyone sees the same signal schedule, so a path with more edges never arrives earlier: comparing edge counts is enough.
- Each node is enqueued at most twice (first and second distance), keeping the BFS linear.
- The second distinct count always exists: you can bounce back and forth on an edge to add 2.
- The simulation waits whenever the light is red at departure, then traverses the edge.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Second path is the shortest + 2 (bouncing on an edge).
- Waiting at red lights.
n = 2with a single edge.
Complexity: O(n + e) time and space.
Medium 1162. As Far from Land as Possible Amazon
Problem: Find a water cell whose distance to the nearest land is maximal (Manhattan), or −1 if there's no land or no water.
grid = [[1,0,1],[0,0,0],[1,0,1]]2grid = [[1,0,0],[0,0,0],[0,0,0]]4Constraints: n == grid.length · n == grid[i].length · 1 ≤ n ≤ 100 · grid[i][j] is 0 or 1
Hint 1
Compute the distance to the nearest land for every cell.
Hint 2
Start BFS from all land cells at once.
From brute force to the pattern: BFS per water cell is O(n⁴); multi-source BFS is O(n²).
Spot it: Distance to the nearest land for every cell → multi-source BFS starting from all land at once; BFS layers are the distances.
Why not another pattern? BFS from each water cell separately is O(n⁴).
Approach:
- Queue all land cells.
- BFS layer by layer into water, marking visited.
- The number of layers processed is the answer (−1 if no water or no land).
from collections import deque
class Solution:
def maxDistance(self, grid: List[List[int]]) -> int:
n = len(grid)
queue = deque((r, c) for r in range(n) for c in range(n) if grid[r][c] == 1)
if len(queue) in (0, n * n):
return -1 # no land, or no water
dist = -1
while queue:
dist += 1
for _ in range(len(queue)):
r, c = queue.popleft()
for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
if 0 <= nr < n and 0 <= nc < n and grid[nr][nc] == 0:
grid[nr][nc] = 1 # reached: mark as visited
queue.append((nr, nc))
return dist
What the code is doing, line by line:
- All land starts at distance 0, so a water cell is first reached at its distance to the nearest land.
- In a grid without obstacles, BFS steps equal Manhattan distance.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All land or all water → −1.
- One land cell.
- Land in the middle.
Complexity: O(n²) time and space.
Medium 1236. Web Crawler Amazon Microsoft
Problem: Using htmlParser.getUrls(url), crawl every page reachable from startUrl on the same hostname (any order).
Solve it on LeetCode ↗ (Premium)
urls = ["http://news.yahoo.com","http://news.yahoo.com/news","http://news.yahoo.com/news/topics/","http://news.google.com","http://news.yahoo.com/us"], edges = [[2,0],[2,1],[3,2],[3,1],[0,4]], startUrl = "http://news.yahoo.com/news/topics/"["http://news.yahoo.com","http://news.yahoo.com/news","http://news.yahoo.com/news/topics/","http://news.yahoo.com/us"]urls = ["http://news.yahoo.com","http://news.yahoo.com/news","http://news.yahoo.com/news/topics/","http://news.google.com"], edges = [[0,2],[2,1],[3,2],[3,1],[3,0]], startUrl = "http://news.google.com"["http://news.google.com"]Constraints: 1 ≤ urls.length ≤ 1000 · 1 ≤ urls[i].length ≤ 300 · startUrl is one of the urls · Hostname label must be from 1 to 63 characters long, including the dots, may contain only the ASCII letters from 'a' to 'z', digits from '0' to '9' and the hyphen-minus character ('-') · You may assume there're no duplicates in the URL library
Hint 1
The pages and links form a graph.
Hint 2
Traverse it, staying on the same hostname.
From brute force to the pattern: This is the plain traversal; the visited set is what makes it finite.
Spot it: Pages and links are a graph; visit everything reachable → BFS (or DFS) with a visited set, filtered by hostname.
Why not another pattern? Recursion without a visited set loops forever on cyclic links.
Approach:
host = startUrl.split('/')[2].- BFS from
startUrl; for each link with the same host and not seen, enqueue it.
from collections import deque
class Solution:
def crawl(self, startUrl: str, htmlParser: "HtmlParser") -> List[str]:
host = startUrl.split("/")[2] # "http://host/path" -> host
seen = {startUrl}
queue = deque([startUrl])
while queue:
url = queue.popleft()
for link in htmlParser.getUrls(url):
if link not in seen and link.split("/")[2] == host:
seen.add(link) # same site, not visited yet
queue.append(link)
return list(seen)
What the code is doing, line by line:
- Splitting on
/gives['http:', '', host, …]. - Marking URLs when enqueued avoids duplicates in the queue.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Links to other hosts (skip).
- Cycles of links.
- Pages with no links.
Complexity: O(V + E) time over pages and links, O(V) space.
Medium 1311. Get Watched Videos by Your Friends Amazon
Problem: Return the videos watched by people at shortest distance exactly level from id, ordered by frequency then name.
watchedVideos = [["A","B"],["C"],["B","C"],["D"]], friends = [[1,2],[0,3],[0,3],[1,2]], id = 0, level = 1["B","C"]watchedVideos = [["A","B"],["C"],["B","C"],["D"]], friends = [[1,2],[0,3],[0,3],[1,2]], id = 0, level = 2["D"]Constraints: n == watchedVideos.length == friends.length · 2 ≤ n ≤ 100 · 1 ≤ watchedVideos[i].length ≤ 100 · 1 ≤ watchedVideos[i][j].length ≤ 8 · 0 ≤ friends[i].length < n · 0 ≤ friends[i][j] < n · 0 ≤ id < n · 1 ≤ level < n · if friends[i] contains j, then friends[j] contains i
Hint 1
Find the people at shortest distance exactly level.
Hint 2
Count their videos and sort.
From brute force to the pattern: DFS can reach people through longer paths; BFS layers are exact.
Spot it: "Friends at distance exactly k" → BFS by layers. Then count videos with a hash map and sort.
Why not another pattern? DFS doesn't give shortest distances.
Approach:
- BFS from
idforlevellayers with a visited set. - Count videos of the people in the final layer.
- Sort by
(count, name).
from collections import Counter
class Solution:
def watchedVideosByFriends(self, watchedVideos: List[List[str]], friends: List[List[int]], id: int, level: int) -> List[str]:
seen = {id}
layer = [id]
for _ in range(level): # move out one layer at a time
nxt = []
for p in layer:
for f in friends[p]:
if f not in seen:
seen.add(f)
nxt.append(f)
layer = nxt
count = Counter(v for p in layer for v in watchedVideos[p])
return sorted(count, key=lambda v: (count[v], v)) # rarest first, then alphabetical
What the code is doing, line by line:
- Marking people as seen when first reached keeps only shortest distances.
- Sorting by
(frequency, name)matches the required order.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Level beyond everyone → [].
- Ties in frequency (alphabetical).
- A person reachable at several distances.
Complexity: O(V + E + W log W) time, where W is the number of distinct videos.
Medium 3552. Grid Teleportation Traversal Amazon
Problem: Move up/down/left/right (cost 1) avoiding #; stepping on a letter lets you teleport to any same letter for free, once per letter. Return the fewest moves.
matrix = [".#...",".#.#.",".#.#.","...#."]13Constraints: 1 ≤ m == matrix.length ≤ 10³ · 1 ≤ n == matrix[i].length ≤ 10³ · matrix[i][j] is either '#', '.', or an uppercase English letter. · matrix[0][0] is not an obstacle.
Hint 1
Teleporting is free and moving costs 1: which BFS handles 0/1 weights?
Hint 2
Is it ever useful to teleport with the same letter twice?
From brute force to the pattern: Tracking used letters in the state is 2²⁶ times bigger; using each letter at its first (cheapest) visit is enough.
Spot it: Edges of weight 0 (teleport) and 1 (move) → 0-1 BFS with a deque. Like Jump Game IV, expand each letter's portal group only the first time that letter is popped (the earliest pop has the smallest distance, so later uses can't help).
Why not another pattern? Plain BFS mishandles zero-cost teleports; tracking which letters were used in the state blows up.
Approach:
- Group cells by letter.
- Deque from (0, 0) with distance 0.
- Pop a cell; if it's an unused letter, set every same-letter cell to this distance (push front).
- Normal moves cost 1 (push back).
from collections import defaultdict, deque
class Solution:
def minMoves(self, matrix: List[str]) -> int:
m, n = len(matrix), len(matrix[0])
portals = defaultdict(list)
for r in range(m):
for c in range(n):
if matrix[r][c].isupper():
portals[matrix[r][c]].append((r, c))
INF = float("inf")
dist = [[INF] * n for _ in range(m)]
dist[0][0] = 0
dq = deque([(0, 0)])
used = set()
while dq:
r, c = dq.popleft()
d = dist[r][c]
if (r, c) == (m - 1, n - 1):
return d
ch = matrix[r][c]
if ch.isupper() and ch not in used:
used.add(ch) # this letter's teleport is spent here, at its cheapest
for pr, pc in portals[ch]:
if d < dist[pr][pc]:
dist[pr][pc] = d
dq.appendleft((pr, pc)) # free move: same distance
for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
if 0 <= nr < m and 0 <= nc < n and matrix[nr][nc] != "#" and d + 1 < dist[nr][nc]:
dist[nr][nc] = d + 1
dq.append((nr, nc))
return -1
What the code is doing, line by line:
- The deque stays sorted by distance: zero-cost teleports go to the front, moves to the back.
- The first time a letter is popped is at its smallest distance, so teleporting from there dominates any later use; the once-per-letter rule never hurts.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Start or end on a portal.
- Unreachable end → −1.
- Portals behind walls.
Complexity: O(m · n) time and space.
Hard 854. K-Similar Strings Amazon
Problem: Return the fewest swaps of two letters to turn s1 into the anagram s2.
s1 = "ab", s2 = "ba"1s1 = "abc", s2 = "bca"2Constraints: 1 ≤ s1.length ≤ 20 · s2.length == s1.length · s1 and s2 contain only lowercase letters from the set {'a', 'b', 'c', 'd', 'e', 'f'}. · s2 is an anagram of s1.
Hint 1
Model each string as a BFS state and each swap as an edge.
Hint 2
Reduce branching: always fix the first wrong position.
From brute force to the pattern: BFS with all O(n²) swaps explodes; fixing the first mismatch keeps the search small.
Spot it: Fewest swaps → BFS where each state is a string. Branching is cut down by always fixing the first mismatch i: swap it with a later j where cur[j] == s2[i] (and preferably cur[j] != s2[j]).
Why not another pattern? Greedy swapping isn't optimal when cycles of length > 2 exist; trying all swaps is far too many states.
Approach:
- Queue starts with
s1, distance 0. - Pop
cur; find the firstiwithcur[i] != s2[i]. - For each
j > iwithcur[j] == s2[i]andcur[j] != s2[j]: swap, enqueue if new. - Return the distance when
cur == s2.
from collections import deque
class Solution:
def kSimilarity(self, s1: str, s2: str) -> int:
seen = {s1}
queue = deque([(s1, 0)])
while queue:
cur, swaps = queue.popleft()
if cur == s2:
return swaps
i = 0
while cur[i] == s2[i]:
i += 1 # first position that's wrong
for j in range(i + 1, len(cur)):
if cur[j] == s2[i] and cur[j] != s2[j]: # brings the right letter to i, and j was wrong anyway
nxt = cur[:i] + cur[j] + cur[i + 1:j] + cur[i] + cur[j + 1:]
if nxt not in seen:
seen.add(nxt)
queue.append((nxt, swaps + 1))
return -1
What the code is doing, line by line:
- Fixing the first mismatch first doesn't lose optimality: some optimal sequence fixes it with one of these swaps.
- Skipping
jthat are already correct avoids pointless moves. - BFS returns the fewest swaps.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Already equal → 0.
- A 2-cycle (one swap).
- Long cycles of letters.
Complexity: Exponential in the worst case but small for length ≤ 20 with this pruning.
Medium 1197. Minimum Knight Moves Apple
Problem: On an infinite board, return the fewest knight moves from (0, 0) to (x, y).
Solve it on LeetCode ↗ (Premium)
x = 2, y = 11x = 5, y = 54Constraints: -300 ≤ x, y ≤ 300 · 0 ≤ |x| + |y| ≤ 300
Hint 1
Shortest number of moves: BFS.
Hint 2
Use symmetry to search only one quadrant (with a small margin).
From brute force to the pattern: Unbounded BFS in all quadrants works but explores 4× more; symmetry trims it.
Spot it: Fewest moves on an unweighted graph → BFS. By symmetry, target (|x|, |y|) and stay in the first quadrant, allowing coordinates down to −2 (some shortest paths dip slightly below the axes).
Why not another pattern? DFS doesn't find shortest paths; an unbounded BFS explores four quadrants for nothing.
Approach:
x, y = |x|, |y|.- BFS from
(0, 0)with the 8 knight moves, skipping cells with a coordinate below −2. - Return the distance when the target is reached.
from collections import deque
class Solution:
def minKnightMoves(self, x: int, y: int) -> int:
x, y = abs(x), abs(y) # all four quadrants look the same
moves = [(1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1), (-2, 1), (-1, 2)]
seen = {(0, 0)}
queue = deque([(0, 0, 0)])
while queue:
r, c, d = queue.popleft()
if (r, c) == (x, y):
return d
for dr, dc in moves:
nr, nc = r + dr, c + dc
if nr >= -2 and nc >= -2 and (nr, nc) not in seen: # small dip below the axes is enough
seen.add((nr, nc))
queue.append((nr, nc, d + 1))
return -1
What the code is doing, line by line:
- Reflecting the target into the first quadrant doesn't change the answer.
- Paths to targets near the origin (like (1, 1)) need to step to negative coordinates, but never below −2.
- BFS returns the shortest distance.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Target (0, 0) → 0.
- (1, 1) → 2.
- Far targets.
Complexity: O(|x| · |y|) time and space.