Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Breadth-First Search (BFS)

Builds on Level-Order BFS and Graph BFS, which you haven’t finished yet.

P1MediumBuilt on Level-Order BFS~6 min speedrun

Shortest Path BFS (Unweighted Grid / Graph)

Built on
Graph BFSEach node also stores dist, the number of moves from the start, which is the fewest because BFS reaches nodes in order of distance.
Grows into
Zero-One BFSState-Space BFS
Unlocks
Dijkstra
The problem

Shortest Path in Binary Matrix (LC 1091)(opens leetcode.com)

You're given an n x n binary grid. Starting at corner cell (0, 0) and finishing at the opposite corner (n - 1, n - 1), find the shortest clear path between them, or report -1 if no such path exists.

A path counts as clear when every cell it visits holds 0, and each consecutive pair of cells touches 8-directionally — sharing a side or merely a corner both count as touching.

A clear path's length is simply how many cells it visits, start to finish.

Example 1
Input:grid = [[0,1],[1,0]]
Output:2
Step diagonally from (0,0) to (1,1): the path visits 2 cells.
0110

Shortest-Path BFS

start cell (0, 0) âž” 8/4-way delta scan âž” target hit (N-1, N-1)
Spot it when: You need the fewest moves through a grid, or through any graph where every move costs the same (knight moves, jumps between array indices).
Shortest-Path BFS
Python
1from collections import deque
2from typing import Callable, Hashable, Iterable, List
3 
4 
5def fewest_steps(start: Hashable, is_goal: Callable[[Hashable], bool], neighbors: Callable[[Hashable], Iterable[Hashable]]) -> int:
6 """Shortest-Path BFS: the fewest moves from start to a cell where is_goal holds, or -1.
7 neighbors(cell) lists the cells one move away that may be entered.
8 Precondition: every move costs one step.
9 V = the cells reached. T = the cost of one is_goal or neighbors call (it lists at most T cells).
10 Time: O(V * T) | Space: O(V) queue and seen
11 """
12 queue = deque([(start, 0)]) # (cell, moves taken to reach it)
13 seen = {start} # cells already queued, so each one is enqueued once
14 
15 while queue:
16 # 1. Pop and Check for the Goal:
17 # Every move costs exactly 1, so the first time is_goal is true, dist is the
18 # shortest distance from start — no later pop can report less.
19 cell, dist = queue.popleft()
20 if is_goal(cell):
21 return dist
22 
23 # 2. Enqueue Every Unseen Neighbour:
24 # Marking a neighbour seen at enqueue time, not at pop time, stops it from
25 # being queued twice while it is still waiting in line.
26 for nxt in neighbors(cell):
27 if nxt not in seen:
28 seen.add(nxt)
29 queue.append((nxt, dist + 1))
30 
31 return -1
Invariant: Cells leave queue in non-decreasing order of dist, and a cell is marked 1 the moment it is first reached, which is by a shortest path.

Treat each cell as a node and each allowed move (4 or 8 directions) as an edge of cost 1. BFS reaches cells in order of distance, so the first time you pop the target, its distance is the shortest. Store the distance together with the cell in the queue. The trap is marking too late: set grid[nr][nc] = 1 when you add a cell to the queue, not when you pop it, or the same cell is queued again and again by its neighbours.

See it run

Shortest-Path BFS, step by step

dist - · queue (0, 0):1

dist 1 · queue empty

dist 1 · queue (0, 1):2

dist 2 · queue empty

dist 2 · queue (0, 2):3 (1, 2):3

dist 3 · queue (1, 2):3

dist 3 · queue (1, 2):3

dist 3 · queue empty

dist 3 · queue (2, 2):4

dist 4 · queue empty

dist 4 · queue empty · answer 4

Each cell of the grid is one cell of the board. # is a wall. A number is the dist the cell was pushed with. A blank cell has not been reached.

  • pop the cell taken from the front of the queue
  • pushed a clear cell that is marked and pushed now
  • seen a neighbour that is already marked, so it is skipped

Start: grid[0][0] is clear. It is marked and pushed with dist = 1, because the path counts cells.

Pop (0, 0) with dist = 1. It is not the bottom-right cell, so look at its neighbours.

Pushed (0, 1) with dist = 2.

Pop (0, 1) with dist = 2. It is not the bottom-right cell, so look at its neighbours.

Pushed (0, 2), (1, 2) with dist = 3.

Pop (0, 2) with dist = 3. It is not the bottom-right cell, so look at its neighbours.

No neighbour is clear and unmarked, so nothing is pushed. (1, 2) is still waiting in the queue but is already marked, so it is not pushed again.

Pop (1, 2) with dist = 3. It is not the bottom-right cell, so look at its neighbours.

Pushed (2, 2) with dist = 4.

Pop (2, 2) with dist = 4. It is the bottom-right cell, so return 4.

Done: the goal was popped first with dist = 4, so the answer is 4.

row 0
col 01pushed
col 1
col 2
row 1
#
#
row 2
#
#
1 / 11
Gotchas1 of 5

Gotcha 1 of 5

  1. 1
    The trap

    Gotcha 1: Marking a cell when it is popped, not when pushed

    Mistake
    r, c, dist = queue.popleft()
    grid[r][c] = 1
    ...
    queue.append((nr, nc, dist + 1))
    Fix
    r, c, dist = queue.popleft()
    ...
    grid[nr][nc] = 1
    queue.append((nr, nc, dist + 1))

    What happens: A cell that is queued but not yet popped still reads 0 in grid. Each of its neighbours that is popped meanwhile pushes it again. So the same cell can sit in the queue many times. The answer is still right, but the queue and the work grow many times over.

  2. 2

    Gotcha 2: Not checking that the start cell is clear

    Mistake
    if grid[n - 1][n - 1] != 0:
    Fix
    if grid[0][0] != 0 or grid[n - 1][n - 1] != 0:

    What happens: The path must start on a clear cell. The loop never checks the start cell, because the start is pushed before the loop. When grid[0][0] is 1, the search still walks from it and can report a path. The answer must be -1.

    Example: grid = [[1,0],[0,0]]
    Mistake: returns 2Fix: returns -1
  3. 3

    Gotcha 3: Giving the start cell distance 0 instead of 1

    Mistake
    queue = deque([(0, 0, 0)])
    Fix
    queue = deque([(0, 0, 1)])

    What happens: Shortest Path in Binary Matrix (LC 1091) counts the cells on the path, not the moves. The start cell is on the path, so its distance is 1. With 0, every answer is one too small. Read how each problem measures length.

    Example: Shortest Path in Binary Matrix (LC 1091): grid = [[0,1],[1,0]]
    Mistake: returns 1Fix: returns 2
  4. 4

    Gotcha 4: Reusing grid after the search has marked it

    Mistake
    shortestPathBinaryMatrix(grid)
    Fix
    shortestPathBinaryMatrix([row[:] for row in grid])

    What happens: The search marks cells by setting them to 1 in grid itself. LeetCode does not read grid again, so this is fine there. Code that uses grid after the call sees the marks as walls. Pass a copy when the caller still needs the original grid.

    Example: grid = [[0,1],[1,0]], then read grid again
    Mistake: returns [[1, 1], [1, 1]]Fix: returns [[0, 1], [1, 0]]
  5. 5

    Gotcha 5: Rescanning same-value indices in Jump Game IV (LC 1345)

    What happens: In Jump Game IV (LC 1345), an index can jump to every index with the same value. After you push the indices in that value's list, clear the list. Otherwise every later index with that value scans the whole list again. With many equal values, BFS then takes O(n^2) time.

Not this one if…
  • Edges have different weights, as in Network Delay Time (LC 743), so the path with the fewest steps is not always the cheapest. See Dijkstra
  • Where you are is a whole lock code, word or board, not a cell, as in Open the Lock (LC 752), and the neighbours of a state must be generated rather than read from a grid. See State-Space BFS

Deep Dive

Why the first arrival at a cell is the shortest path to it

Graph BFS (visit-once) already marks a cell when it is pushed, so each cell enters queue once. This card adds one number to each entry: dist, the number of cells on the path from the start. The question is why that first dist is also the smallest.

Every move costs one step. Pop a cell with dist = d. Each cell it pushes gets dist = d + 1 and goes to the back of queue. So queue holds cells with dist equal to d, then cells with dist equal to d + 1, and the pops come out in non-decreasing order of dist.

A cell is first reached from the first popped cell that lists it. Every cell with a smaller dist was popped earlier and did not list it. So no shorter path exists, and the stored dist is final. The first time the bottom-right cell is popped, its dist is the answer.

Example: in Shortest Path in Binary Matrix (LC 1091), the grid [[0,0,0],[1,1,0],[1,1,0]] pops (0, 0) with dist = 1, then (0, 1) with 2, then (0, 2) and (1, 2) with 3, then (2, 2) with 4. The answer is 4.

One trace: a later sighting never lowers dist

The board replays the grid from the previous section. A # is a wall. A number is the dist a cell was pushed with.

Pop (0, 1) with dist = 2. It pushes (0, 2) and (1, 2), and both get dist = 3.

Now pop (0, 2). Its neighbour (1, 2) is clear, but it already has dist = 3, so the loop skips it. This is the step marked as the trap on the board.

Skipping is correct. (0, 2) has dist = 3, so a path to (1, 2) through it would have dist = 4. That is larger than the 3 already stored. A later sighting never gives a smaller value, so do not overwrite it.

The cost: cells times work per cell

The card's complexity line is O(V * T). V is the number of cells that are reached. T is the work to list the neighbours of one cell and test one of them.

In shortestPathBinaryMatrix, the grid has N * N cells. Each cell is pushed at most once, as in Graph BFS. Each pop checks 8 neighbours, and every check is O(1). So T is a constant, and the total is O(N^2) time.

queue and the marks in grid hold at most one entry per cell, so the space is O(N^2). The blueprint marks the cells inside grid, which adds no extra set.

Example: an empty 3 by 3 grid pushes 9 cells, and an empty 6 by 6 grid pushes 36. The count follows N * N, as the formula says.

What dist counts, and which problems fit

The generic template fewest_steps starts with dist = 0 and counts moves. Shortest Path in Binary Matrix (LC 1091) counts the cells on the path, and the start cell is on the path. So the blueprint starts with dist = 1. Read how each problem measures length before you choose the start value.

Example: for [[0,1],[1,0]], the path is (0, 0) then (1, 1). That is 2 cells and 1 move. The right answer is 2, and starting with dist = 0 returns 1.

The loop never tests the start cell, because the start is pushed before the loop. The guard grid[0][0] != 0 must therefore run first. For [[1,0],[0,0]] the right answer is -1, and a search without that guard returns 2.

The same template fits any graph where every move costs the same. Jump Game IV (LC 1345) lists the neighbours of index i as i - 1, i + 1, and every index with the same value. Only the neighbors function changes.

If the moves cost different amounts, as in Network Delay Time (LC 743), the first pop of the goal is not always the cheapest path. Use a cheapest-first search instead.

60-Second Pitch

"Every move costs one step, so I use BFS from the start cell. It is the usual graph BFS that marks a cell when it is pushed, and each entry in queue also carries its dist. BFS pops cells in non-decreasing order of dist. So the first time I pop the bottom-right cell, its dist is the shortest path, and no later pop can be smaller. Each cell is pushed once, and each pop checks 8 neighbours. That gives O(N^2) time and O(N^2) space for an N by N grid. I check the start and end cells first, because the loop never tests the start. If queue empties, I return -1."

Common Questions

Why use BFS and not DFS to find the shortest path?

DFS follows one path to its end before it tries another. The first path it finds to the goal is not always the shortest, so it would have to try every path. BFS reaches cells in order of dist, so the first time it reaches the goal, that dist is the smallest.

Why is the first dist of a cell the smallest?

Cells leave queue in non-decreasing order of dist. The first popped cell that lists a neighbour has the smallest dist of all cells that list it, so the neighbour gets that dist plus 1. A later sighting can only give an equal or larger value, so it is skipped.

What are the time and space complexity?

O(N^2) time and O(N^2) space for an N by N grid. Each cell is pushed once, and each pop looks at 8 neighbours in constant time. In the generic template it is O(V * T), where V is the number of reached cells and T is the cost of one neighbors call.

How do you return the path and not only its length?

Store a parent for each cell when you push it, which is the cell that was popped at that time. When the goal is popped, follow parent back to the start and reverse the list. Each cell has one parent because it is pushed only once.

What changes when the moves have different costs?

The first pop of the goal is then not always the cheapest, because a path with more steps can cost less. If the costs are only 0 and 1, a deque (double-ended queue) can keep the order. For general positive costs, use a cheapest-first search with a heap, as in Network Delay Time (LC 743).

SWE Reasoning

Spot it

  • The question asks for the fewest moves, steps or jumps from one start to one target.
  • Every move costs the same: one step in a grid, one jump, one knight move.
  • The state of the search is a position that a grid or a list of neighbours already describes. Nothing else changes between visits.
  • A DFS (depth-first search) would find some path, but you would have to try every path to prove which one is shortest.
  • The statement says to return -1 when the target cannot be reached. BFS ends with an empty queue in that case.

The invariant, step by step

  1. The guard checks that grid[0][0] and grid[n - 1][n - 1] are clear. Then queue holds (0, 0, 1), and grid[0][0] is set to 1.
  2. The loop pops the front of queue. If it is the bottom-right cell, it returns dist.
  3. For each of the 8 directions, a neighbour that is inside the grid and equal to 0 is marked as 1 and pushed with dist + 1. This is the mark on push of Graph BFS (visit-once).
  4. A marked cell is never pushed again, so its first dist is never replaced.
  5. The pushed cells go to the back, so queue holds cells with dist equal to d and then d + 1. The pops come out in non-decreasing order of dist.
  6. The first pop of the bottom-right cell therefore has the smallest possible dist. If queue becomes empty first, the corner cannot be reached, and the function returns -1.

Traps

  • Marking at pop time. This is the trap of Graph BFS (visit-once). Here it also means a cell can be pushed twice with different dist values. Set grid[nr][nc] = 1 when you push.
  • Missing start check. The loop never tests grid[0][0]. For [[1,0],[0,0]] the search returns 2 instead of -1. Check both corners before the loop.
  • Wrong start value. In Shortest Path in Binary Matrix (LC 1091), the start cell counts, so dist starts at 1. For [[0,1],[1,0]], starting at 0 returns 1 instead of 2.
  • Mutating the caller's grid. The search writes 1 into grid. Code that reads grid after the call sees the visited cells as walls. Pass [row[:] for row in grid] when the grid is needed again.
  • Same-value scans in Jump Game IV (LC 1345). An index can jump to every index with its value. If the list of those indices is not cleared after it is used, every later index with that value scans it again, and the time becomes O(n^2).

Practice

Canonical LeetCode:Shortest Path in Binary Matrix (LC 1091)Minimum Knight Moves (LC 1197)Shortest Path to Get Food (LC 1730)Nearest Exit from Entrance in Maze (LC 1926)Jump Game IV (LC 1345)Minimum Moves to Move a Box to Their Target Location (LC 1263)
Theory Context•Breadth-First Search (BFS)

Builds on Level-Order BFS and Graph BFS, which you haven’t finished yet.

P1MediumBuilt on Level-Order BFS~6 min speedrun

Shortest Path BFS (Unweighted Grid / Graph)

Built on
Graph BFSEach node also stores dist, the number of moves from the start, which is the fewest because BFS reaches nodes in order of distance.
Grows into
Zero-One BFSState-Space BFS
Unlocks
Dijkstra
The problem

Shortest Path in Binary Matrix (LC 1091)(opens leetcode.com)

You're given an n x n binary grid. Starting at corner cell (0, 0) and finishing at the opposite corner (n - 1, n - 1), find the shortest clear path between them, or report -1 if no such path exists.

A path counts as clear when every cell it visits holds 0, and each consecutive pair of cells touches 8-directionally — sharing a side or merely a corner both count as touching.

A clear path's length is simply how many cells it visits, start to finish.

Example 1
Input:grid = [[0,1],[1,0]]
Output:2
Step diagonally from (0,0) to (1,1): the path visits 2 cells.
0110

Shortest-Path BFS

start cell (0, 0) âž” 8/4-way delta scan âž” target hit (N-1, N-1)
Spot it when: You need the fewest moves through a grid, or through any graph where every move costs the same (knight moves, jumps between array indices).
Shortest-Path BFS
Python
1from collections import deque
2from typing import Callable, Hashable, Iterable, List
3 
4 
5def fewest_steps(start: Hashable, is_goal: Callable[[Hashable], bool], neighbors: Callable[[Hashable], Iterable[Hashable]]) -> int:
6 """Shortest-Path BFS: the fewest moves from start to a cell where is_goal holds, or -1.
7 neighbors(cell) lists the cells one move away that may be entered.
8 Precondition: every move costs one step.
9 V = the cells reached. T = the cost of one is_goal or neighbors call (it lists at most T cells).
10 Time: O(V * T) | Space: O(V) queue and seen
11 """
12 queue = deque([(start, 0)]) # (cell, moves taken to reach it)
13 seen = {start} # cells already queued, so each one is enqueued once
14 
15 while queue:
16 # 1. Pop and Check for the Goal:
17 # Every move costs exactly 1, so the first time is_goal is true, dist is the
18 # shortest distance from start — no later pop can report less.
19 cell, dist = queue.popleft()
20 if is_goal(cell):
21 return dist
22 
23 # 2. Enqueue Every Unseen Neighbour:
24 # Marking a neighbour seen at enqueue time, not at pop time, stops it from
25 # being queued twice while it is still waiting in line.
26 for nxt in neighbors(cell):
27 if nxt not in seen:
28 seen.add(nxt)
29 queue.append((nxt, dist + 1))
30 
31 return -1
Invariant: Cells leave queue in non-decreasing order of dist, and a cell is marked 1 the moment it is first reached, which is by a shortest path.

Treat each cell as a node and each allowed move (4 or 8 directions) as an edge of cost 1. BFS reaches cells in order of distance, so the first time you pop the target, its distance is the shortest. Store the distance together with the cell in the queue. The trap is marking too late: set grid[nr][nc] = 1 when you add a cell to the queue, not when you pop it, or the same cell is queued again and again by its neighbours.

See it run

Shortest-Path BFS, step by step

dist - · queue (0, 0):1

dist 1 · queue empty

dist 1 · queue (0, 1):2

dist 2 · queue empty

dist 2 · queue (0, 2):3 (1, 2):3

dist 3 · queue (1, 2):3

dist 3 · queue (1, 2):3

dist 3 · queue empty

dist 3 · queue (2, 2):4

dist 4 · queue empty

dist 4 · queue empty · answer 4

Each cell of the grid is one cell of the board. # is a wall. A number is the dist the cell was pushed with. A blank cell has not been reached.

  • pop the cell taken from the front of the queue
  • pushed a clear cell that is marked and pushed now
  • seen a neighbour that is already marked, so it is skipped

Start: grid[0][0] is clear. It is marked and pushed with dist = 1, because the path counts cells.

Pop (0, 0) with dist = 1. It is not the bottom-right cell, so look at its neighbours.

Pushed (0, 1) with dist = 2.

Pop (0, 1) with dist = 2. It is not the bottom-right cell, so look at its neighbours.

Pushed (0, 2), (1, 2) with dist = 3.

Pop (0, 2) with dist = 3. It is not the bottom-right cell, so look at its neighbours.

No neighbour is clear and unmarked, so nothing is pushed. (1, 2) is still waiting in the queue but is already marked, so it is not pushed again.

Pop (1, 2) with dist = 3. It is not the bottom-right cell, so look at its neighbours.

Pushed (2, 2) with dist = 4.

Pop (2, 2) with dist = 4. It is the bottom-right cell, so return 4.

Done: the goal was popped first with dist = 4, so the answer is 4.

row 0
col 01pushed
col 1
col 2
row 1
#
#
row 2
#
#
1 / 11
Gotchas1 of 5

Gotcha 1 of 5

  1. 1
    The trap

    Gotcha 1: Marking a cell when it is popped, not when pushed

    Mistake
    r, c, dist = queue.popleft()
    grid[r][c] = 1
    ...
    queue.append((nr, nc, dist + 1))
    Fix
    r, c, dist = queue.popleft()
    ...
    grid[nr][nc] = 1
    queue.append((nr, nc, dist + 1))

    What happens: A cell that is queued but not yet popped still reads 0 in grid. Each of its neighbours that is popped meanwhile pushes it again. So the same cell can sit in the queue many times. The answer is still right, but the queue and the work grow many times over.

  2. 2

    Gotcha 2: Not checking that the start cell is clear

    Mistake
    if grid[n - 1][n - 1] != 0:
    Fix
    if grid[0][0] != 0 or grid[n - 1][n - 1] != 0:

    What happens: The path must start on a clear cell. The loop never checks the start cell, because the start is pushed before the loop. When grid[0][0] is 1, the search still walks from it and can report a path. The answer must be -1.

    Example: grid = [[1,0],[0,0]]
    Mistake: returns 2Fix: returns -1
  3. 3

    Gotcha 3: Giving the start cell distance 0 instead of 1

    Mistake
    queue = deque([(0, 0, 0)])
    Fix
    queue = deque([(0, 0, 1)])

    What happens: Shortest Path in Binary Matrix (LC 1091) counts the cells on the path, not the moves. The start cell is on the path, so its distance is 1. With 0, every answer is one too small. Read how each problem measures length.

    Example: Shortest Path in Binary Matrix (LC 1091): grid = [[0,1],[1,0]]
    Mistake: returns 1Fix: returns 2
  4. 4

    Gotcha 4: Reusing grid after the search has marked it

    Mistake
    shortestPathBinaryMatrix(grid)
    Fix
    shortestPathBinaryMatrix([row[:] for row in grid])

    What happens: The search marks cells by setting them to 1 in grid itself. LeetCode does not read grid again, so this is fine there. Code that uses grid after the call sees the marks as walls. Pass a copy when the caller still needs the original grid.

    Example: grid = [[0,1],[1,0]], then read grid again
    Mistake: returns [[1, 1], [1, 1]]Fix: returns [[0, 1], [1, 0]]
  5. 5

    Gotcha 5: Rescanning same-value indices in Jump Game IV (LC 1345)

    What happens: In Jump Game IV (LC 1345), an index can jump to every index with the same value. After you push the indices in that value's list, clear the list. Otherwise every later index with that value scans the whole list again. With many equal values, BFS then takes O(n^2) time.

Not this one if…
  • Edges have different weights, as in Network Delay Time (LC 743), so the path with the fewest steps is not always the cheapest. See Dijkstra
  • Where you are is a whole lock code, word or board, not a cell, as in Open the Lock (LC 752), and the neighbours of a state must be generated rather than read from a grid. See State-Space BFS

Deep Dive

Why the first arrival at a cell is the shortest path to it

Graph BFS (visit-once) already marks a cell when it is pushed, so each cell enters queue once. This card adds one number to each entry: dist, the number of cells on the path from the start. The question is why that first dist is also the smallest.

Every move costs one step. Pop a cell with dist = d. Each cell it pushes gets dist = d + 1 and goes to the back of queue. So queue holds cells with dist equal to d, then cells with dist equal to d + 1, and the pops come out in non-decreasing order of dist.

A cell is first reached from the first popped cell that lists it. Every cell with a smaller dist was popped earlier and did not list it. So no shorter path exists, and the stored dist is final. The first time the bottom-right cell is popped, its dist is the answer.

Example: in Shortest Path in Binary Matrix (LC 1091), the grid [[0,0,0],[1,1,0],[1,1,0]] pops (0, 0) with dist = 1, then (0, 1) with 2, then (0, 2) and (1, 2) with 3, then (2, 2) with 4. The answer is 4.

One trace: a later sighting never lowers dist

The board replays the grid from the previous section. A # is a wall. A number is the dist a cell was pushed with.

Pop (0, 1) with dist = 2. It pushes (0, 2) and (1, 2), and both get dist = 3.

Now pop (0, 2). Its neighbour (1, 2) is clear, but it already has dist = 3, so the loop skips it. This is the step marked as the trap on the board.

Skipping is correct. (0, 2) has dist = 3, so a path to (1, 2) through it would have dist = 4. That is larger than the 3 already stored. A later sighting never gives a smaller value, so do not overwrite it.

The cost: cells times work per cell

The card's complexity line is O(V * T). V is the number of cells that are reached. T is the work to list the neighbours of one cell and test one of them.

In shortestPathBinaryMatrix, the grid has N * N cells. Each cell is pushed at most once, as in Graph BFS. Each pop checks 8 neighbours, and every check is O(1). So T is a constant, and the total is O(N^2) time.

queue and the marks in grid hold at most one entry per cell, so the space is O(N^2). The blueprint marks the cells inside grid, which adds no extra set.

Example: an empty 3 by 3 grid pushes 9 cells, and an empty 6 by 6 grid pushes 36. The count follows N * N, as the formula says.

What dist counts, and which problems fit

The generic template fewest_steps starts with dist = 0 and counts moves. Shortest Path in Binary Matrix (LC 1091) counts the cells on the path, and the start cell is on the path. So the blueprint starts with dist = 1. Read how each problem measures length before you choose the start value.

Example: for [[0,1],[1,0]], the path is (0, 0) then (1, 1). That is 2 cells and 1 move. The right answer is 2, and starting with dist = 0 returns 1.

The loop never tests the start cell, because the start is pushed before the loop. The guard grid[0][0] != 0 must therefore run first. For [[1,0],[0,0]] the right answer is -1, and a search without that guard returns 2.

The same template fits any graph where every move costs the same. Jump Game IV (LC 1345) lists the neighbours of index i as i - 1, i + 1, and every index with the same value. Only the neighbors function changes.

If the moves cost different amounts, as in Network Delay Time (LC 743), the first pop of the goal is not always the cheapest path. Use a cheapest-first search instead.

60-Second Pitch

"Every move costs one step, so I use BFS from the start cell. It is the usual graph BFS that marks a cell when it is pushed, and each entry in queue also carries its dist. BFS pops cells in non-decreasing order of dist. So the first time I pop the bottom-right cell, its dist is the shortest path, and no later pop can be smaller. Each cell is pushed once, and each pop checks 8 neighbours. That gives O(N^2) time and O(N^2) space for an N by N grid. I check the start and end cells first, because the loop never tests the start. If queue empties, I return -1."

Common Questions

Why use BFS and not DFS to find the shortest path?

DFS follows one path to its end before it tries another. The first path it finds to the goal is not always the shortest, so it would have to try every path. BFS reaches cells in order of dist, so the first time it reaches the goal, that dist is the smallest.

Why is the first dist of a cell the smallest?

Cells leave queue in non-decreasing order of dist. The first popped cell that lists a neighbour has the smallest dist of all cells that list it, so the neighbour gets that dist plus 1. A later sighting can only give an equal or larger value, so it is skipped.

What are the time and space complexity?

O(N^2) time and O(N^2) space for an N by N grid. Each cell is pushed once, and each pop looks at 8 neighbours in constant time. In the generic template it is O(V * T), where V is the number of reached cells and T is the cost of one neighbors call.

How do you return the path and not only its length?

Store a parent for each cell when you push it, which is the cell that was popped at that time. When the goal is popped, follow parent back to the start and reverse the list. Each cell has one parent because it is pushed only once.

What changes when the moves have different costs?

The first pop of the goal is then not always the cheapest, because a path with more steps can cost less. If the costs are only 0 and 1, a deque (double-ended queue) can keep the order. For general positive costs, use a cheapest-first search with a heap, as in Network Delay Time (LC 743).

SWE Reasoning

Spot it

  • The question asks for the fewest moves, steps or jumps from one start to one target.
  • Every move costs the same: one step in a grid, one jump, one knight move.
  • The state of the search is a position that a grid or a list of neighbours already describes. Nothing else changes between visits.
  • A DFS (depth-first search) would find some path, but you would have to try every path to prove which one is shortest.
  • The statement says to return -1 when the target cannot be reached. BFS ends with an empty queue in that case.

The invariant, step by step

  1. The guard checks that grid[0][0] and grid[n - 1][n - 1] are clear. Then queue holds (0, 0, 1), and grid[0][0] is set to 1.
  2. The loop pops the front of queue. If it is the bottom-right cell, it returns dist.
  3. For each of the 8 directions, a neighbour that is inside the grid and equal to 0 is marked as 1 and pushed with dist + 1. This is the mark on push of Graph BFS (visit-once).
  4. A marked cell is never pushed again, so its first dist is never replaced.
  5. The pushed cells go to the back, so queue holds cells with dist equal to d and then d + 1. The pops come out in non-decreasing order of dist.
  6. The first pop of the bottom-right cell therefore has the smallest possible dist. If queue becomes empty first, the corner cannot be reached, and the function returns -1.

Traps

  • Marking at pop time. This is the trap of Graph BFS (visit-once). Here it also means a cell can be pushed twice with different dist values. Set grid[nr][nc] = 1 when you push.
  • Missing start check. The loop never tests grid[0][0]. For [[1,0],[0,0]] the search returns 2 instead of -1. Check both corners before the loop.
  • Wrong start value. In Shortest Path in Binary Matrix (LC 1091), the start cell counts, so dist starts at 1. For [[0,1],[1,0]], starting at 0 returns 1 instead of 2.
  • Mutating the caller's grid. The search writes 1 into grid. Code that reads grid after the call sees the visited cells as walls. Pass [row[:] for row in grid] when the grid is needed again.
  • Same-value scans in Jump Game IV (LC 1345). An index can jump to every index with its value. If the list of those indices is not cleared after it is used, every later index with that value scans it again, and the time becomes O(n^2).

Practice

Canonical LeetCode:Shortest Path in Binary Matrix (LC 1091)Minimum Knight Moves (LC 1197)Shortest Path to Get Food (LC 1730)Nearest Exit from Entrance in Maze (LC 1926)Jump Game IV (LC 1345)Minimum Moves to Move a Box to Their Target Location (LC 1263)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•