DSA sheet · Graphs · Multi-source BFS, level by level

Rotting Oranges

Rot spreads from every rotten orange to its neighbours, one minute at a time. "How many minutes until everything is rotten?" is really "how many BFS layers does it take?". The teacher uses this problem to show three things: why the word minimum here doesn't mean DP, why all rotten oranges start together (multi-source BFS), and how to count minutes by processing the queue one level at a time, exactly like level order traversal in a binary tree. A small fresh counter then tells us whether some orange could never be reached.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

Graph words in one place

For normal graphs we read an edge list and build an adjacency list (each node → list of neighbours):

edge list → adjacency list
n = 3
edges = [[0, 1], [1, 2]]
adj = [[] for _ in range(n)]
for u, v in edges:
    adj[u].append(v)
    adj[v].append(u)
print(adj)   # [[1], [0, 2], [1]]

A grid is a graph without an adjacency list

Every cell (r, c) is a node. Its neighbours are up, down, left, right. The teacher's point: an adjacency list is for when we don't know a node's children. In a grid we always do, by adding (dr, dc) offsets, so no list is needed.

            (r-1,c)
               ↑
  (r,c-1) ← (r,c) → (r,c+1)        directions = (0,-1) (-1,0) (0,1) (1,0)
               ↓
            (r+1,c)

Always check bounds before reading a neighbour: 0 <= nr < m and 0 <= nc < n.

Visited, and a free way to mark it

Without a "visited" mark, BFS would keep revisiting the same cells. The teacher's checklist always asks: do I need a separate visited array, or can a value in the grid tell me? In many problems you must build one. Here, turning a fresh orange (1) into rotten (2) is the visited mark. We only ever step into 1s, so a cell that's now 2 is never entered again.

A connected component is a group of cells that can reach each other. Empty cells (0) break the grid into pieces. A fresh orange in a piece with no rotten orange can never rot, so the answer becomes −1.

BFS and DFS

Why BFS layers = shortest time

The queue gets cells in order of distance: everything at distance d is popped before anything at distance d+1, because new cells are always added at the back with distance d+1. So the first time BFS reaches a cell is the earliest possible moment. In this problem that is exactly what happens in real life too: an orange rots the first minute any rotten neighbour touches it, never later. So BFS layer number = the minute it rots.

Multi-source BFS: why all sources start in the queue together

If several oranges are rotten at minute 0, they all spread at the same time. The teacher stresses this: it isn't one orange spreading and then the next one. Every rotten orange infects its neighbours in the same minute.

R = rotten, F = fresh             all R start together      start from the left R only
R F F F F R                       minute 1: R R F F R R     minute 1: R R F F F R
                                  minute 2: R R R R R R     ...
                                  answer 2 ✓                 minute 4: R R R R R R → 4 ✗

Putting every rotten orange in the queue before the loop starts makes layer 0 = "all rotten at the start". Then layer 1 = everything one step from any of them, and so on. That copies the real spreading exactly. Starting from one at a time would either give a wrong (too large) time, or need a whole new BFS per rotten orange.

Doubt: all three LeetCode examples have only one rotten orange. Why plan for many?
→ The teacher checks this explicitly: the question never promises just one. Since there can be many, treat it as multi-source. With exactly one, the same code simply behaves like single-source BFS.

Processing a queue level by level

To count minutes we need to know where one layer ends. Trick from level order traversal of a binary tree: at the start of each round, read size = len(queue). Exactly those size items belong to the current level. Pop only that many. Anything pushed during the round is the next level and waits for the next round.

level-by-level BFS template
from collections import deque

adj = [[1, 2], [0, 3], [0], [1]]
queue = deque([0])
seen = {0}
level = 0
while queue:
    size = len(queue)                # how many nodes are in THIS level
    names = []
    for _ in range(size):
        u = queue.popleft()
        names.append(u)
        for v in adj[u]:
            if v not in seen:
                seen.add(v)
                queue.append(v)      # goes to the NEXT level
    print(level, names)
    level += 1
# prints:
# 0 [0]
# 1 [1, 2]
# 2 [3]

Part A · Multi-source BFS, one level per minute

LeetCode 994

1The question in simple words

You get an m×n grid. Each cell holds:

Every minute, each fresh orange that touches a rotten one (up, down, left or right, not diagonally) becomes rotten. Return the minimum number of minutes until no fresh orange is left. If that can never happen, return −1.

Example 1 → 4
2 1 1
1 1 0
0 1 1
Example 2 → −1
2 1 1
0 1 1
1 0 1
bottom-left is cut off
Example 3 → 0
0 2
no fresh orange
at all

Only rotten oranges spread rot. A fresh orange next to another fresh orange stays fresh.

2What the constraints tell us

3Intuition: rings of rot

Picture every rotten orange as a drop of ink. Each minute, every drop spreads one cell outwards, all at once. The answer is the number of rings needed to cover the last fresh orange. That's BFS, started from all rotten oranges at once, counting layers.

The teacher's 3-question checklist:

questionanswerwhy
Adjacency list?NoGrid: neighbours come from row ± 1 and column ± 1.
BFS or DFS?BFSWe want the minimum time and each step costs exactly 1 minute.
Visited array?No extra arrayChanging 1 → 2 marks the cell. We only enter cells that are 1.

4Building the logic from her examples

"Minimum" doesn't mean DP

The teacher warns: seeing the word "minimum" doesn't automatically mean dynamic programming. When moving from one cell to the next always costs the same (1 minute), plain BFS gives the minimum. Only when costs differ (weighted edges) do we need something more, like Dijkstra.

Why not DFS?

DFS would push the rot down one long winding path first, then come back and try other directions. The time it reports depends on which direction it happened to pick first, and it can report a much larger time than the real one. A cell next to the start might get reached at the very end of a long path. In real life rot spreads in all directions at once, which is BFS.

2 1 1          DFS going right first:  (0,1)=1 → (0,2)=2 → (1,2)=3 → (1,1)=4 → (1,0)=5 ...
1 1 1          but (1,0) actually rots at minute 1 (it touches the 2 directly)
1 1 1          BFS gives each cell its real minute

Starting the queue: every 2 goes in

Scan the whole grid. Every rotten orange's (row, col) is pushed into the queue. At the same time, count the fresh oranges (we'll see why below). minutes starts at 0: the oranges rotten from the start don't cost any time.

One minute = one level of the queue

The teacher's dry run on Example 1:

So we must pop exactly one level per minute. That's the size = len(queue) trick from Part 0. If we didn't freeze the size, the children pushed during this minute would be popped in the same round and we'd lose track of minutes.

Stepping into a neighbour

For each popped cell, try 4 directions. Enter (nr, nc) only if it's inside the grid and grid[nr][nc] == 1 (a fresh orange). Then:

  1. set grid[nr][nc] = 2 (it's rotten now, and also marked visited),
  2. decrease fresh by 1,
  3. push it, so next minute it spreads rot further.
Doubt: why don't we also skip 0s and 2s explicitly?
→ The check grid[nr][nc] == 1 already does. A 0 is an empty cell (nothing to rot, no reason to go there). A 2 is already rotten, the teacher's "already visited, don't go again".

How do we know it's impossible? The fresh counter

When the queue becomes empty, BFS just stops. It doesn't say whether every orange rotted. The teacher's example: imagine a fresh orange surrounded by empty cells. No rot can reach it, the queue empties, and that orange is still fresh.

Fix: count fresh oranges at the start, subtract one each time one rots. At the end:

The loop condition: while queue and fresh > 0

Look at Example 1 at the end: the last fresh orange (2,2) rots at minute 4, and it is pushed into the queue. If the loop kept going, it would pop (2,2), find nothing to rot, and still add +1 minute at the end of that level. The answer would become 5.

The teacher adds fresh > 0 to the loop: once every fresh orange has rotted, there's nothing left to do, even if the queue still holds cells. Those cells can't find any fresh orange. Her reason is "don't waste time", but it matters for correctness too: it stops the extra minute.

Doubt: what if I write while queue only?
→ Then the very last level (oranges that rot nothing) still counts as a minute, so you're off by one. You'd have to answer minutes - 1 (and handle the "no rotten at all" case separately). The version below shows the bug, and the test file checks it.
the bug: no fresh > 0 check (don't use this)
from collections import deque

def oranges_without_fresh_check(grid):
    m, n = len(grid), len(grid[0])
    queue = deque()
    fresh = 0
    for i in range(m):
        for j in range(n):
            if grid[i][j] == 2:
                queue.append((i, j))
            elif grid[i][j] == 1:
                fresh += 1
    minutes = 0
    while queue:                                  # no "fresh > 0" here
        for _ in range(len(queue)):
            r, c = queue.popleft()
            for dr, dc in [(0, -1), (-1, 0), (0, 1), (1, 0)]:
                nr, nc = r + dr, c + dc
                if 0 <= nr < m and 0 <= nc < n and grid[nr][nc] == 1:
                    grid[nr][nc] = 2
                    fresh -= 1
                    queue.append((nr, nc))
        minutes += 1
    return minutes if fresh == 0 else -1

print(oranges_without_fresh_check([[2, 1, 1], [1, 1, 0], [0, 1, 1]]))   # 5
print(oranges_without_fresh_check([[0, 2]]))                            # 1

Both are wrong: the right answers are 4 and 0. The last level always adds one minute too many.

Don't count the starting rotten oranges as a minute

The teacher points this out in her dry run: the orange at (0,0) was already rotten. It didn't take a minute to rot, so level 0 adds nothing by itself. Minutes only count spreading: level 1 (minute 1), level 2 (minute 2)…, and the answer is the minute the last fresh orange rots.

5Approach steps

  1. Scan the grid: push every 2 into the queue; count every 1 as fresh.
  2. minutes = 0.
  3. While the queue isn't empty and fresh > 0:
  4.   freeze size = len(queue) and pop exactly that many cells;
  5.   for each, look 4 ways; every fresh neighbour → set to 2, fresh -= 1, push;
  6.   after the level, minutes += 1.
  7. Return minutes if fresh == 0, else −1.

6Code (Python)

Rotting Oranges · multi-source BFS, level by level
from collections import deque

class Solution:
    def orangesRotting(self, grid):
        m, n = len(grid), len(grid[0])
        queue = deque()
        fresh = 0
        for i in range(m):
            for j in range(n):
                if grid[i][j] == 2:
                    queue.append((i, j))        # every rotten orange is a source
                elif grid[i][j] == 1:
                    fresh += 1

        minutes = 0
        directions = [(0, -1), (-1, 0), (0, 1), (1, 0)]
        while queue and fresh > 0:
            size = len(queue)                   # oranges that spread THIS minute
            for _ in range(size):
                r, c = queue.popleft()
                for dr, dc in directions:
                    nr, nc = r + dr, c + dc
                    if 0 <= nr < m and 0 <= nc < n and grid[nr][nc] == 1:
                        grid[nr][nc] = 2        # rots now (and is marked visited)
                        fresh -= 1
                        queue.append((nr, nc))
            minutes += 1                        # one whole level = one minute

        return minutes if fresh == 0 else -1

7Code line by line

linewhat it means
if grid[i][j] == 2: queue.append((i, j))All rotten oranges start together: multi-source.
elif grid[i][j] == 1: fresh += 1Count what still needs to rot. (In the video she first typed == 0 here and corrected it to 1: we count fresh oranges, not empty cells.)
minutes = 0Oranges rotten at the start cost no time.
while queue and fresh > 0:Keep spreading while there's something spreading and something left to rot. The second part stops the extra minute.
size = len(queue)Freeze the size of this level. Only these cells spread during this minute.
r, c = queue.popleft()One rotten orange of this minute.
0 <= nr < m and 0 <= nc < nNeighbour inside the grid.
grid[nr][nc] == 1Only fresh oranges can rot. 0 = empty, 2 = already rotten (visited).
grid[nr][nc] = 2It rots. This is also our visited mark, so no other orange pushes it again.
fresh -= 1One less orange to worry about.
queue.append((nr, nc))Next minute, this orange spreads too.
minutes += 1A whole level finished: one minute passed.
return minutes if fresh == 0 else -1Some fresh orange left → it was unreachable → −1.
Doubt: this changes the input grid. Is that allowed?
→ The teacher chose it on purpose to save the visited array. If the interviewer doesn't want the input changed, copy the grid first, or keep a separate visited set. Same idea, more memory.

8Dry run (her Example 1)

grid               index map
2 1 1              (0,0) (0,1) (0,2)
1 1 0              (1,0) (1,1) (1,2)
0 1 1              (2,0) (2,1) (2,2)
start: queue = [(0,0)], fresh = 6, minutes = 0
minute (level)poppedfresh neighbours rottedqueue AFTER the levelfresh afterminutes
1(0,0)(0,1), (1,0)(0,1) (1,0)41
2(0,1)(0,2), (1,1)(0,2) (1,1)22
(1,0)none: (1,1) already 2, (2,0) is empty
3(0,2)none: (1,2) is empty(2,1)13
(1,1)(2,1)
4(2,1)(2,2)(2,2)04
stopfresh is 0 → the loop ends (even though (2,2) is still in the queue) → return 4
after minute 1
2 2 1
2 1 0
0 1 1
after minute 2
2 2 2
2 2 0
0 1 1
after minute 4
2 2 2
2 2 0
0 2 2

In the video she reads a couple of cell indexes a bit loosely while pointing at the board. The table above uses the exact indexes.

Example 2 quickly: the bottom-left 1 at (2,0) has a 0 above it and a 0 to its right. No rotten orange can ever touch it. BFS rots the other 5, the queue runs dry, fresh is still 1 → −1.

Example 3: [[0, 2]] has no fresh orange, so fresh = 0, the loop never runs → 0.

9Complexity & remember

RememberAll 2s in the queue first + count the 1s. while queue and fresh > 0: pop one level, rot fresh neighbours (1 → 2, fresh −1, push), then minutes + 1. End: fresh == 0 ? minutes : −1.

Part B · Revision page

01 MatrixRotting Oranges
sourcesevery 0every 2
what we tracka distance per cellone number: the number of levels
visited markdist = −1 → not visitedgrid value 1 → not visited (set to 2)
level by level?not needed (dist = parent + 1)yes, size = len(queue) per minute
impossible casenone (at least one 0)fresh > 0 at the end → −1
gridanswerwhy
no oranges at all0nothing fresh, nothing to wait for
only rotten0fresh = 0 from the start
fresh but no rotten−1queue is empty, fresh stays > 0
a fresh orange walled off by 0s−1never reached
If you remember only 5 lines 1. Equal 1-minute steps → BFS, not DP, not DFS.
2. Push all rotten oranges first; count fresh ones.
3. One level of the queue = one minute (size = len(queue)).
4. Rot = set to 2, fresh −1, push. 2 doubles as visited.
5. Loop while queue and fresh > 0; return minutes or −1.
Mistakes to avoid ✗ starting BFS from only one rotten orange
✗ adding a minute per popped orange instead of per level
✗ while queue without fresh > 0 (one extra minute)
✗ forgetting the fresh counter (can't detect −1)
✗ counting the starting rotten oranges as a minute
✗ diagonals (only 4 directions)
test it yourself (paste under the solution above)
s = Solution()
print(s.orangesRotting([[2, 1, 1], [1, 1, 0], [0, 1, 1]]))   # 4
print(s.orangesRotting([[2, 1, 1], [0, 1, 1], [1, 0, 1]]))   # -1
print(s.orangesRotting([[0, 2]]))                            # 0
print(s.orangesRotting([[0]]))                               # 0
print(s.orangesRotting([[1]]))                               # -1
print(s.orangesRotting([[2, 1, 1, 1, 2]]))                   # 2

Based on this video: Rotting Oranges | Multi-source BFS