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, which you haven’t finished yet.

P0MediumBuilt on Level-Order BFS~8 min speedrun
Built on
Level-Order BFSEvery source starts in queue at the same time, so all sources spread together.
The problem

Rotting Oranges (LC 994)(opens leetcode.com)

You're given an m x n grid whose cells each hold one of three values: 0 for an empty cell, 1 for a fresh orange, and 2 for a rotten one.

Each minute, rot spreads: any fresh orange touching a rotten orange along one of the four cardinal directions turns rotten too.

Work out how many minutes must pass before every orange is rotten, and return -1 if some fresh orange can never be reached.

Example 1
Input:grid = [[2,1,1],[1,1,0],[0,1,1]]
Output:4
Rot spreads one step per minute; the orange in the bottom-right corner is the last to turn, at minute 4.
211110011

Multi-Source BFS

all origin sources (t=0) ➔ concentric expansion ➔ shortest time
Spot it when: Many starting points spread at the same speed, and you need the time until everything is reached or each cell's distance to its nearest source.
Multi-Source BFS
Python
1from collections import deque
2from typing import Callable, Dict, Hashable, Iterable, List
3 
4 
5def multi_source_bfs(sources: Iterable[Hashable], neighbors: Callable[[Hashable], Iterable[Hashable]]) -> Dict[Hashable, int]:
6 """Multi-Source BFS: the distance from every reachable cell to its nearest source.
7 Precondition: every move costs one step.
8 Every source starts at distance 0. neighbors(cell) yields the cells one step away that may be entered.
9 V = reachable cells. E = neighbour steps tried. T = the cost of one neighbors call.
10 Time: O(V * T + E) | Space: O(V)
11 """
12 dist = {} # cell -> distance to its nearest source; doubles as the seen set
13 # 1. Seed Every Source at Distance 0:
14 # Placing every source in dist before the search starts lets them all spread
15 # together, instead of one source at a time.
16 for source in sources:
17 dist[source] = 0
18 queue = deque(dist) # holds cells queued for this or a later ring, in enqueue order
19 
20 while queue:
21 # 2. Pop and Expand This Ring's Cell:
22 # Every move costs exactly 1 step, so the first time a cell is reached, it is
23 # reached by its nearest source, at the correct distance.
24 cell = queue.popleft()
25 for nxt in neighbors(cell):
26 if nxt not in dist:
27 # Marking a cell seen (giving it a dist entry) at enqueue time, not at
28 # pop time, stops it from being queued twice while still waiting.
29 dist[nxt] = dist[cell] + 1
30 queue.append(nxt)
31 
32 return dist
Invariant: After minutes rounds, queue holds exactly the oranges that turned rotten in round minutes, and fresh_count counts the fresh oranges not reached yet.

Put every source into the queue at time 0, then run ordinary level-by-level BFS. All sources spread one ring per round, so each cell is reached first by its nearest source, and the round in which it turns is its distance to that source. The trap is the last round: count minutes only while fresh_count > 0, or the final round, which reaches nothing new, adds one minute too many.

See it run

Multi-Source BFS on grid = [[2, 1, 1], [1, 1, 0], [0, 1, 1]], step by step

minutes 0 · fresh_count 6 · len(queue) 1

minutes 1 · fresh_count 6 · len(queue) 1

minutes 1 · fresh_count 4 · len(queue) 2

minutes 2 · fresh_count 4 · len(queue) 2

minutes 2 · fresh_count 3 · len(queue) 2

minutes 2 · fresh_count 2 · len(queue) 2

minutes 3 · fresh_count 2 · len(queue) 2

minutes 3 · fresh_count 1 · len(queue) 2

minutes 3 · fresh_count 1 · len(queue) 1

minutes 4 · fresh_count 1 · len(queue) 1

minutes 4 · fresh_count 0 · len(queue) 1

minutes 4 · fresh_count 0 · len(queue) 1

answer 4

The board is the grid. A cell holds 2 when rotten, 1 when fresh and 0 when empty. A row is one grid row.

  • source a cell that is 2 at minute 0
  • pop the cell removed from queue now
  • rots a fresh cell that becomes 2 in this step
  • queued a cell waiting in queue

Every 2 goes into queue at minute 0: (0, 0). fresh_count = 6.

minutes becomes 1. range(len(queue)) fixes this round at 1 cell.

popleft() returns (0, 0). It rots (1, 0) and (0, 1), so fresh_count is 4.

minutes becomes 2. range(len(queue)) fixes this round at 2 cells.

popleft() returns (1, 0). It rots (1, 1), so fresh_count is 3.

popleft() returns (0, 1). It rots (0, 2), so fresh_count is 2.

minutes becomes 3. range(len(queue)) fixes this round at 2 cells.

popleft() returns (1, 1). It rots (2, 1), so fresh_count is 1.

popleft() returns (0, 2). It has no fresh neighbour, so fresh_count stays 1.

minutes becomes 4. range(len(queue)) fixes this round at 1 cell.

popleft() returns (2, 1). It rots (2, 2), so fresh_count is 0.

fresh_count is 0, but queue still holds 1 cell. A while queue loop would count one more minute.

The loop stops because fresh_count is 0, so the function returns minutes = 4.

row 0
02source
11
21
row 1
1
1
0
row 2
0
1
1
1 / 13
Gotchas1 of 5

Gotcha 1 of 5

  1. 1
    The trap

    Gotcha 1: Looping while queue without fresh_count > 0

    Mistake
    while queue:
    Fix
    while queue and fresh_count > 0:

    What happens: Each round adds one minute. The oranges that rot in the last useful round still go into queue. With while queue, the loop runs one more round for them. That round reaches no fresh orange, but it still adds one minute.

    Example: Rotting Oranges (LC 994): grid = [[2,1,1],[1,1,0],[0,1,1]]
    Mistake: returns 5Fix: returns 4
  2. 2

    Gotcha 2: Seeding the queue with only one rotten orange

    Mistake
    if grid[r][c] == 2 and not queue:
    Fix
    if grid[r][c] == 2:

    What happens: All rotten oranges spread at the same time. Each one must be in queue at minute 0. With one source, the rot spreads only from that orange. A fresh orange next to another rotten orange then waits for the first source to reach it. The answer is too large.

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

    Gotcha 3: Returning minutes without checking fresh_count

    Mistake
    return minutes
    Fix
    return minutes if fresh_count == 0 else -1

    What happens: A fresh orange with no path from any rotten orange never rots. The loop then stops with fresh_count > 0. Rotting Oranges (LC 994) asks for -1 in that case. return minutes returns the number of rounds the loop ran instead.

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

    Gotcha 4: Keeping one minutes counter when each cell needs a distance

    What happens: Map of Highest Peak (LC 1765) and 01 Matrix (LC 542) ask for every cell's distance, not one time. One minutes counter cannot hold that. Store dist for each cell instead. Set it when the cell is pushed, to its parent's dist + 1.

  5. 5

    Gotcha 5: Searching downhill from every cell in Pacific Atlantic Water Flow

    What happens: Pacific Atlantic Water Flow (LC 417) asks which cells can drain into both oceans. A search from every cell repeats the same work for each cell. Run the search backwards instead. Start from each ocean's border and move uphill. The answer is the cells reached from both oceans.

Not this one if…
  • There is one start cell and one target, as in Shortest Path in Binary Matrix (LC 1091), so nothing has to spread from many places at once. See Shortest-Path BFS

Deep Dive

Why starting from every source finds the nearest one

Level-Order BFS takes one start node, and round k handles the nodes at distance k from it. Multi-source BFS changes only the start. Put every source in queue before the first round. Then round k handles the cells at distance k from the nearest source.

Why the nearest source wins: every move costs one step, and the queue keeps its first in, first out order. A cell is added by the first neighbour that is popped and sees it as fresh. That neighbour is in the earliest possible round, so it is closest to some source. Any later source reaches the cell in a later round, and by then the cell is no longer fresh.

In Rotting Oranges (LC 994) with grid = [[2, 1, 1], [1, 1, 0], [0, 1, 1]], the only source is (0, 0). Round 1 rots (0, 1) and (1, 0). Round 2 rots (0, 2) and (1, 1). Round 3 rots (2, 1). Round 4 rots (2, 2). So minutes is 4.

Where minutes is counted, and the trap in the last round

minutes += 1 runs at the start of each round, before the cells of that round are popped. for _ in range(len(queue)) freezes the round size, as level_size does in Level-Order BFS. A cell that rots in this round is added behind the frozen cells, so it infects its neighbours in the next round.

Look at the last round of the example. In round 4, (2, 1) rots (2, 2), and fresh_count becomes 0. The queue still holds (2, 2). If the loop is while queue:, a fifth round starts. It pops (2, 2), finds no fresh neighbour, and still adds 1 to minutes. The result is 5 and not 4.

The loop while queue and fresh_count > 0 stops when nothing fresh is left. So minutes counts only the rounds that rotted at least one orange. If no orange is fresh at the start, fresh_count == 0 returns 0 before the loop.

Reachability, and the exact meaning of fresh_count

fresh_count is the number of fresh oranges that have not rotted yet. It goes down by 1 at the moment a cell is marked rotten and added to queue. Marking the cell at this moment stops a second neighbour from rotting it again in the same round.

When the queue empties and fresh_count is still above 0, some fresh orange had no path of fresh cells from any source. For grid = [[2, 1, 1], [0, 1, 1], [1, 0, 1]], the orange at (2, 0) has only empty neighbours, so the answer is -1. return minutes if fresh_count == 0 else -1 makes this case explicit. Without the check, the function returns 5, the number of rounds it ran, which is wrong.

Seeding every source matters. For grid = [[2, 1, 1], [1, 1, 1], [1, 1, 2]], seeding both rotten cells gives 2 minutes. Seeding only the first gives 3.

The cost, and when you need dist for every cell

Each cell is added at most once, and each pop tries 4 neighbours. For a grid with R rows and C columns, that is O(R * C) time. queue can hold O(R * C) cells, so the space is O(R * C). The card's template states O(V * T + E) for any graph.

Rotting Oranges (LC 994) needs one number, the time until the last orange rots, so a minutes counter is enough. Walls and Gates (LC 286), 01 Matrix (LC 542) and Map of Highest Peak (LC 1765) need the distance of every cell. For those, store dist[nxt] = dist[cell] + 1 when the cell is added. dist also works as the seen set.

Some problems ask for the reverse direction. Pacific Atlantic Water Flow (LC 417) asks which cells drain to both oceans. Start from the border cells of each ocean and move uphill, and keep the cells reached from both. This avoids a search from every cell.

60-Second Pitch

"Every rotten orange spreads at the same speed, so I use BFS with all of them in the queue at minute 0. Each round, I freeze len(queue), add one to minutes, and let every frozen cell rot its fresh neighbours. I mark a neighbour rotten when I add it, and I lower fresh_count. I stop when fresh_count reaches 0, so the last round, which rots nothing, is not counted. If the queue empties while fresh_count is above 0, I return -1. Each cell is added once and tries four neighbours. So the time is O(R * C), and the space is O(R * C) for the queue."

Common Questions

Why is one BFS with all sources better than one BFS per source?

One BFS per source repeats work and costs O(S * R * C) for S sources. One BFS with all sources in the queue visits each cell once, so it costs O(R * C). It is also correct, because each cell is reached first by its nearest source.

What are the time and space complexity of orangesRotting?

O(R * C) time, because each cell is added at most once and tries 4 neighbours. O(R * C) space for queue in the worst case, such as a grid where every cell is rotten at the start.

Why do you stop when fresh_count is 0 and not when queue is empty?

The oranges that rot in the last useful round are added to queue. If you wait for queue to empty, one more round runs. It rots nothing, but it still adds one to minutes. Checking fresh_count > 0 stops before that round.

How do you get the distance of every cell, as in 01 Matrix (LC 542)?

Seed every cell with value 0 in queue and set dist to 0 for them. When a cell is popped, give each unseen neighbour dist[cell] + 1 and add it. A cell has a dist entry once it is added, so dist is also the seen set.

What if the moves have different costs?

BFS is no longer correct, because the first time a cell is reached may not be the cheapest. Use Dijkstra's algorithm with a priority queue, and start it with all sources at distance 0. If the costs are only 0 or 1, a double-ended queue (deque) also works.

SWE Reasoning

Spot it

  • There are many starting points, and they all spread at the same speed, such as rotting, fire, flooding or signal strength.
  • The question asks for the time until everything is reached, or for each cell's distance to its nearest source.
  • Running a single-source BFS from every cell would repeat work, and it could take O(R * C) searches.
  • Every move costs the same, so the first time a cell is reached is the shortest.
  • Some cells may be unreachable, and the problem asks you to report that, for example with -1.

The invariant, step by step

  1. Before the loop, every rotten orange is in queue, and fresh_count counts the fresh oranges. At this moment minutes is 0.
  2. A round starts with minutes += 1. range(len(queue)) freezes the number of cells that were rotten at the end of the last round.
  3. Each frozen cell is popped, and its fresh neighbours are marked 2. For each one, fresh_count goes down by 1 and the neighbour is added to queue.
  4. When the round ends, the frozen cells are gone, and queue holds exactly the oranges that rotted in this round. This is the invariant for the new value of minutes.
  5. The loop stops when queue is empty or fresh_count is 0. With fresh_count > 0 in the loop condition, a round that rots nothing is never counted.
  6. If fresh_count == 0, return minutes. Otherwise some fresh orange could not be reached, and the answer is -1.

Traps

  • Last round counted. while queue: runs one more round after fresh_count reaches 0. The result is one minute too large.
  • Only one source seeded. A fresh orange next to another rotten orange then waits for the first source. The answer is too large.
  • Missing reachability check. return minutes returns the number of rounds when some fresh orange is never reached. The correct answer is -1.
  • Cell marked when popped. A fresh cell can then be added twice by two neighbours in the same round, and fresh_count goes down twice. Mark the cell when you add it.
  • No fresh orange at the start. The grid may already be fully rotten or empty. Return 0 before the loop.
  • One counter where a distance per cell is needed. Walls and Gates (LC 286) and 01 Matrix (LC 542) need dist for each cell.

Practice

Canonical LeetCode:Rotting Oranges (LC 994)Walls and Gates (LC 286)Map of Highest Peak (LC 1765)Pacific Atlantic Water Flow (LC 417)Shortest Bridge (LC 934)01 Matrix (LC 542)As Far from Land as Possible (LC 1162)
Theory Context•Breadth-First Search (BFS)

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

P0MediumBuilt on Level-Order BFS~8 min speedrun
Built on
Level-Order BFSEvery source starts in queue at the same time, so all sources spread together.
The problem

Rotting Oranges (LC 994)(opens leetcode.com)

You're given an m x n grid whose cells each hold one of three values: 0 for an empty cell, 1 for a fresh orange, and 2 for a rotten one.

Each minute, rot spreads: any fresh orange touching a rotten orange along one of the four cardinal directions turns rotten too.

Work out how many minutes must pass before every orange is rotten, and return -1 if some fresh orange can never be reached.

Example 1
Input:grid = [[2,1,1],[1,1,0],[0,1,1]]
Output:4
Rot spreads one step per minute; the orange in the bottom-right corner is the last to turn, at minute 4.
211110011

Multi-Source BFS

all origin sources (t=0) ➔ concentric expansion ➔ shortest time
Spot it when: Many starting points spread at the same speed, and you need the time until everything is reached or each cell's distance to its nearest source.
Multi-Source BFS
Python
1from collections import deque
2from typing import Callable, Dict, Hashable, Iterable, List
3 
4 
5def multi_source_bfs(sources: Iterable[Hashable], neighbors: Callable[[Hashable], Iterable[Hashable]]) -> Dict[Hashable, int]:
6 """Multi-Source BFS: the distance from every reachable cell to its nearest source.
7 Precondition: every move costs one step.
8 Every source starts at distance 0. neighbors(cell) yields the cells one step away that may be entered.
9 V = reachable cells. E = neighbour steps tried. T = the cost of one neighbors call.
10 Time: O(V * T + E) | Space: O(V)
11 """
12 dist = {} # cell -> distance to its nearest source; doubles as the seen set
13 # 1. Seed Every Source at Distance 0:
14 # Placing every source in dist before the search starts lets them all spread
15 # together, instead of one source at a time.
16 for source in sources:
17 dist[source] = 0
18 queue = deque(dist) # holds cells queued for this or a later ring, in enqueue order
19 
20 while queue:
21 # 2. Pop and Expand This Ring's Cell:
22 # Every move costs exactly 1 step, so the first time a cell is reached, it is
23 # reached by its nearest source, at the correct distance.
24 cell = queue.popleft()
25 for nxt in neighbors(cell):
26 if nxt not in dist:
27 # Marking a cell seen (giving it a dist entry) at enqueue time, not at
28 # pop time, stops it from being queued twice while still waiting.
29 dist[nxt] = dist[cell] + 1
30 queue.append(nxt)
31 
32 return dist
Invariant: After minutes rounds, queue holds exactly the oranges that turned rotten in round minutes, and fresh_count counts the fresh oranges not reached yet.

Put every source into the queue at time 0, then run ordinary level-by-level BFS. All sources spread one ring per round, so each cell is reached first by its nearest source, and the round in which it turns is its distance to that source. The trap is the last round: count minutes only while fresh_count > 0, or the final round, which reaches nothing new, adds one minute too many.

See it run

Multi-Source BFS on grid = [[2, 1, 1], [1, 1, 0], [0, 1, 1]], step by step

minutes 0 · fresh_count 6 · len(queue) 1

minutes 1 · fresh_count 6 · len(queue) 1

minutes 1 · fresh_count 4 · len(queue) 2

minutes 2 · fresh_count 4 · len(queue) 2

minutes 2 · fresh_count 3 · len(queue) 2

minutes 2 · fresh_count 2 · len(queue) 2

minutes 3 · fresh_count 2 · len(queue) 2

minutes 3 · fresh_count 1 · len(queue) 2

minutes 3 · fresh_count 1 · len(queue) 1

minutes 4 · fresh_count 1 · len(queue) 1

minutes 4 · fresh_count 0 · len(queue) 1

minutes 4 · fresh_count 0 · len(queue) 1

answer 4

The board is the grid. A cell holds 2 when rotten, 1 when fresh and 0 when empty. A row is one grid row.

  • source a cell that is 2 at minute 0
  • pop the cell removed from queue now
  • rots a fresh cell that becomes 2 in this step
  • queued a cell waiting in queue

Every 2 goes into queue at minute 0: (0, 0). fresh_count = 6.

minutes becomes 1. range(len(queue)) fixes this round at 1 cell.

popleft() returns (0, 0). It rots (1, 0) and (0, 1), so fresh_count is 4.

minutes becomes 2. range(len(queue)) fixes this round at 2 cells.

popleft() returns (1, 0). It rots (1, 1), so fresh_count is 3.

popleft() returns (0, 1). It rots (0, 2), so fresh_count is 2.

minutes becomes 3. range(len(queue)) fixes this round at 2 cells.

popleft() returns (1, 1). It rots (2, 1), so fresh_count is 1.

popleft() returns (0, 2). It has no fresh neighbour, so fresh_count stays 1.

minutes becomes 4. range(len(queue)) fixes this round at 1 cell.

popleft() returns (2, 1). It rots (2, 2), so fresh_count is 0.

fresh_count is 0, but queue still holds 1 cell. A while queue loop would count one more minute.

The loop stops because fresh_count is 0, so the function returns minutes = 4.

row 0
02source
11
21
row 1
1
1
0
row 2
0
1
1
1 / 13
Gotchas1 of 5

Gotcha 1 of 5

  1. 1
    The trap

    Gotcha 1: Looping while queue without fresh_count > 0

    Mistake
    while queue:
    Fix
    while queue and fresh_count > 0:

    What happens: Each round adds one minute. The oranges that rot in the last useful round still go into queue. With while queue, the loop runs one more round for them. That round reaches no fresh orange, but it still adds one minute.

    Example: Rotting Oranges (LC 994): grid = [[2,1,1],[1,1,0],[0,1,1]]
    Mistake: returns 5Fix: returns 4
  2. 2

    Gotcha 2: Seeding the queue with only one rotten orange

    Mistake
    if grid[r][c] == 2 and not queue:
    Fix
    if grid[r][c] == 2:

    What happens: All rotten oranges spread at the same time. Each one must be in queue at minute 0. With one source, the rot spreads only from that orange. A fresh orange next to another rotten orange then waits for the first source to reach it. The answer is too large.

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

    Gotcha 3: Returning minutes without checking fresh_count

    Mistake
    return minutes
    Fix
    return minutes if fresh_count == 0 else -1

    What happens: A fresh orange with no path from any rotten orange never rots. The loop then stops with fresh_count > 0. Rotting Oranges (LC 994) asks for -1 in that case. return minutes returns the number of rounds the loop ran instead.

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

    Gotcha 4: Keeping one minutes counter when each cell needs a distance

    What happens: Map of Highest Peak (LC 1765) and 01 Matrix (LC 542) ask for every cell's distance, not one time. One minutes counter cannot hold that. Store dist for each cell instead. Set it when the cell is pushed, to its parent's dist + 1.

  5. 5

    Gotcha 5: Searching downhill from every cell in Pacific Atlantic Water Flow

    What happens: Pacific Atlantic Water Flow (LC 417) asks which cells can drain into both oceans. A search from every cell repeats the same work for each cell. Run the search backwards instead. Start from each ocean's border and move uphill. The answer is the cells reached from both oceans.

Not this one if…
  • There is one start cell and one target, as in Shortest Path in Binary Matrix (LC 1091), so nothing has to spread from many places at once. See Shortest-Path BFS

Deep Dive

Why starting from every source finds the nearest one

Level-Order BFS takes one start node, and round k handles the nodes at distance k from it. Multi-source BFS changes only the start. Put every source in queue before the first round. Then round k handles the cells at distance k from the nearest source.

Why the nearest source wins: every move costs one step, and the queue keeps its first in, first out order. A cell is added by the first neighbour that is popped and sees it as fresh. That neighbour is in the earliest possible round, so it is closest to some source. Any later source reaches the cell in a later round, and by then the cell is no longer fresh.

In Rotting Oranges (LC 994) with grid = [[2, 1, 1], [1, 1, 0], [0, 1, 1]], the only source is (0, 0). Round 1 rots (0, 1) and (1, 0). Round 2 rots (0, 2) and (1, 1). Round 3 rots (2, 1). Round 4 rots (2, 2). So minutes is 4.

Where minutes is counted, and the trap in the last round

minutes += 1 runs at the start of each round, before the cells of that round are popped. for _ in range(len(queue)) freezes the round size, as level_size does in Level-Order BFS. A cell that rots in this round is added behind the frozen cells, so it infects its neighbours in the next round.

Look at the last round of the example. In round 4, (2, 1) rots (2, 2), and fresh_count becomes 0. The queue still holds (2, 2). If the loop is while queue:, a fifth round starts. It pops (2, 2), finds no fresh neighbour, and still adds 1 to minutes. The result is 5 and not 4.

The loop while queue and fresh_count > 0 stops when nothing fresh is left. So minutes counts only the rounds that rotted at least one orange. If no orange is fresh at the start, fresh_count == 0 returns 0 before the loop.

Reachability, and the exact meaning of fresh_count

fresh_count is the number of fresh oranges that have not rotted yet. It goes down by 1 at the moment a cell is marked rotten and added to queue. Marking the cell at this moment stops a second neighbour from rotting it again in the same round.

When the queue empties and fresh_count is still above 0, some fresh orange had no path of fresh cells from any source. For grid = [[2, 1, 1], [0, 1, 1], [1, 0, 1]], the orange at (2, 0) has only empty neighbours, so the answer is -1. return minutes if fresh_count == 0 else -1 makes this case explicit. Without the check, the function returns 5, the number of rounds it ran, which is wrong.

Seeding every source matters. For grid = [[2, 1, 1], [1, 1, 1], [1, 1, 2]], seeding both rotten cells gives 2 minutes. Seeding only the first gives 3.

The cost, and when you need dist for every cell

Each cell is added at most once, and each pop tries 4 neighbours. For a grid with R rows and C columns, that is O(R * C) time. queue can hold O(R * C) cells, so the space is O(R * C). The card's template states O(V * T + E) for any graph.

Rotting Oranges (LC 994) needs one number, the time until the last orange rots, so a minutes counter is enough. Walls and Gates (LC 286), 01 Matrix (LC 542) and Map of Highest Peak (LC 1765) need the distance of every cell. For those, store dist[nxt] = dist[cell] + 1 when the cell is added. dist also works as the seen set.

Some problems ask for the reverse direction. Pacific Atlantic Water Flow (LC 417) asks which cells drain to both oceans. Start from the border cells of each ocean and move uphill, and keep the cells reached from both. This avoids a search from every cell.

60-Second Pitch

"Every rotten orange spreads at the same speed, so I use BFS with all of them in the queue at minute 0. Each round, I freeze len(queue), add one to minutes, and let every frozen cell rot its fresh neighbours. I mark a neighbour rotten when I add it, and I lower fresh_count. I stop when fresh_count reaches 0, so the last round, which rots nothing, is not counted. If the queue empties while fresh_count is above 0, I return -1. Each cell is added once and tries four neighbours. So the time is O(R * C), and the space is O(R * C) for the queue."

Common Questions

Why is one BFS with all sources better than one BFS per source?

One BFS per source repeats work and costs O(S * R * C) for S sources. One BFS with all sources in the queue visits each cell once, so it costs O(R * C). It is also correct, because each cell is reached first by its nearest source.

What are the time and space complexity of orangesRotting?

O(R * C) time, because each cell is added at most once and tries 4 neighbours. O(R * C) space for queue in the worst case, such as a grid where every cell is rotten at the start.

Why do you stop when fresh_count is 0 and not when queue is empty?

The oranges that rot in the last useful round are added to queue. If you wait for queue to empty, one more round runs. It rots nothing, but it still adds one to minutes. Checking fresh_count > 0 stops before that round.

How do you get the distance of every cell, as in 01 Matrix (LC 542)?

Seed every cell with value 0 in queue and set dist to 0 for them. When a cell is popped, give each unseen neighbour dist[cell] + 1 and add it. A cell has a dist entry once it is added, so dist is also the seen set.

What if the moves have different costs?

BFS is no longer correct, because the first time a cell is reached may not be the cheapest. Use Dijkstra's algorithm with a priority queue, and start it with all sources at distance 0. If the costs are only 0 or 1, a double-ended queue (deque) also works.

SWE Reasoning

Spot it

  • There are many starting points, and they all spread at the same speed, such as rotting, fire, flooding or signal strength.
  • The question asks for the time until everything is reached, or for each cell's distance to its nearest source.
  • Running a single-source BFS from every cell would repeat work, and it could take O(R * C) searches.
  • Every move costs the same, so the first time a cell is reached is the shortest.
  • Some cells may be unreachable, and the problem asks you to report that, for example with -1.

The invariant, step by step

  1. Before the loop, every rotten orange is in queue, and fresh_count counts the fresh oranges. At this moment minutes is 0.
  2. A round starts with minutes += 1. range(len(queue)) freezes the number of cells that were rotten at the end of the last round.
  3. Each frozen cell is popped, and its fresh neighbours are marked 2. For each one, fresh_count goes down by 1 and the neighbour is added to queue.
  4. When the round ends, the frozen cells are gone, and queue holds exactly the oranges that rotted in this round. This is the invariant for the new value of minutes.
  5. The loop stops when queue is empty or fresh_count is 0. With fresh_count > 0 in the loop condition, a round that rots nothing is never counted.
  6. If fresh_count == 0, return minutes. Otherwise some fresh orange could not be reached, and the answer is -1.

Traps

  • Last round counted. while queue: runs one more round after fresh_count reaches 0. The result is one minute too large.
  • Only one source seeded. A fresh orange next to another rotten orange then waits for the first source. The answer is too large.
  • Missing reachability check. return minutes returns the number of rounds when some fresh orange is never reached. The correct answer is -1.
  • Cell marked when popped. A fresh cell can then be added twice by two neighbours in the same round, and fresh_count goes down twice. Mark the cell when you add it.
  • No fresh orange at the start. The grid may already be fully rotten or empty. Return 0 before the loop.
  • One counter where a distance per cell is needed. Walls and Gates (LC 286) and 01 Matrix (LC 542) need dist for each cell.

Practice

Canonical LeetCode:Rotting Oranges (LC 994)Walls and Gates (LC 286)Map of Highest Peak (LC 1765)Pacific Atlantic Water Flow (LC 417)Shortest Bridge (LC 934)01 Matrix (LC 542)As Far from Land as Possible (LC 1162)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•