Coding Drills Β· 07

Grids & Matrices

A 2D grid is a graph in disguise: every cell is a node and every shared side is an edge. Seven drills teach the four moves that solve almost every grid question β€” DFS flood fill, level-by-level BFS, reverse search from the target, and boundary-shrinking simulation β€” each with a traced example, a complete Java solution, and the bugs interviewers watch for.

7
Drills Β· 65–71
1
Easy
6
Medium
0
Hard
01
Chapter One Β· Core Idea

A Grid Is a Graph You Never Build

Grid problems look like their own category, but almost all of them are graph problems where the graph is never built. A matrix of m rows and n columns is a graph with mΒ·n nodes; each cell (r, c) has up to four neighbours β€” (r-1, c), (r+1, c), (r, c-1), (r, c+1) β€” or eight if diagonals count. You never allocate adjacency lists: a small direction array plus a bounds check generates the neighbours on demand. That single observation lets you transfer everything you know about DFS and BFS directly onto 2D arrays.

What changes from problem to problem is only what you are asked about the graph: how many connected regions there are (DFS), how far something is (BFS), which cells can reach a border (search backwards from the border), or a fixed visiting order that is not a search at all (simulation with shrinking boundaries). The drills on this page cover each of those questions, in increasing order of subtlety.

A grid is an implicit graph β€” neighbours are computed, never stored
THE GRID ↑ ← r,c β†’ ↓ 4 neighbours: (rΒ±1, c), (r, cΒ±1) same thing as an implicit graph THE GRAPH 00 01 02 10 11 12 20 21 22 node = cell Β· edge = shared side V = mΒ·n nodes, E β‰ˆ 2Β·mΒ·n edges β€” so any full traversal is O(mΒ·n)
Analogy: Think of a city laid out on a perfect street grid. You never need a map listing "which blocks touch which" β€” you know that from any block you can walk north, south, east or west. Grid algorithms work the same way: the coordinates are the map, and the direction array {{1,0},{-1,0},{0,1},{0,-1}} is your compass.
🧭

The direction array

Store the deltas once β€” int[][] DIRS = {{1,0},{-1,0},{0,1},{0,-1}} β€” and loop over it. Eight-way problems just add the four diagonals. One loop replaces four copy-pasted blocks, and copy-paste is where off-by-one bugs live.

🚧

The bounds check comes first

Always test r >= 0 && r < rows && c >= 0 && c < cols before reading grid[r][c]. Java's && short-circuits, so the array access is never reached for an out-of-range cell.

βœ…

Visited marking

Either overwrite the cell in place (sink land to water, paint the colour, rot the orange) or keep a parallel boolean[][] seen. In-place is O(1) extra space but destroys the input β€” say so in an interview.

πŸ“

Rows vs columns

grid.length is the number of rows; grid[0].length is the number of columns. Non-square inputs (3Γ—4, 1Γ—n, nΓ—1) are the classic way hidden tests break a solution that was only tried on squares.

02
Chapter Two Β· Mental Model

How to Recognise the Pattern

Before writing a line of code, translate the question into graph language. The cause β†’ effect chain below is the reasoning you should be able to say out loud in an interview; the decision diagram underneath maps each kind of question to the technique that answers it.

Each cell has at most 4 (or 8) neighbours, computable from its coordinates
β–Ά
No adjacency list is needed β€” a direction array plus a bounds check enumerates edges in O(1) per cell
Every cell is marked visited the first time it is reached
β–Ά
Each cell is processed once and each edge examined a constant number of times β€” total O(mΒ·n) regardless of DFS or BFS
DFS commits to one direction and only backtracks at a dead end
β–Ά
One top-level call explores exactly one connected component β€” perfect for counting islands or measuring their area
BFS expands in rings: all cells at distance k before any at distance k+1
β–Ά
The first time BFS touches a cell is along a shortest path β€” the right tool for minimum steps or minutes
Seeding the BFS queue with many sources at once
β–Ά
Each cell receives its distance to the nearest source in a single pass β€” no need to run BFS once per source
The rule is easier to check backwards (β€œcan water flow from here to the ocean?”)
β–Ά
Start from the target cells and walk the inverted rule β€” one search instead of one search per starting cell
The order of visiting is fixed by geometry, not by connectivity
β–Ά
It is a simulation: keep explicit boundaries (top, bottom, left, right) and shrink them after each pass
What does the question ask about cells? Count, size or paint connected regions? yes DFS flood fill recursion explores one whole component at a time Drills 65 Β· 66 Β· 67 Minimum steps / minutes from one or many sources? yes BFS by levels first time BFS reaches a cell = fewest steps Drills 68 Β· 69 Which cells can reach a border or a target set? yes Reverse search start from the targets, walk the rule backwards Drill 70 Visit cells in a fixed geometric order? yes Boundary simulation no search at all β€” four walls that move inward Drill 71
Rule of thumb: if the answer is a count or a size, DFS is usually shortest to write. If the answer is a distance or a time, you need BFS β€” DFS can find a path, but not the shortest one, without exploring everything.
03
Chapter Three Β· Pattern Toolkit

Four Templates for Every Grid

Four templates cover every drill on this page. Learn them as shapes: once you can write each one from memory in under two minutes, the drills reduce to choosing a template and deciding what "visited" and "neighbour" mean.

Template 1 β€” Recursive DFS flood fill
Explore one whole connected region. Guard first (bounds, wrong value, already visited), then mark, then recurse in every direction.
Pseudocode
function dfs(grid, r, c): if r < 0 or r >= rows or c < 0 or c >= cols: return // 1. bounds if grid[r][c] != TARGET: return // 2. wrong value / visited grid[r][c] = VISITED // 3. mark BEFORE recursing for each (dr, dc) in DIRS: dfs(grid, r + dr, c + dc) // 4. explore neighbours for each cell (r, c): if grid[r][c] == TARGET: components = components + 1 // new region found dfs(grid, r, c)
DFS goes deep first β€” the call stack is as tall as the path it is following
1 = land Β· number = DFS visit order 1 2 0 0 0 3 4 0 0 0 5 6 order tried at each cell: down, up, right, left CALL STACK AT THE DEEPEST POINT dfs(0,0) dfs(0,1) dfs(1,1) dfs(1,2) dfs(2,2) dfs(2,3) ← top: (2,3) has no unvisited land left, so calls start returning depth = 6 = island size worst case O(mΒ·n) frames
Template 2 β€” BFS by levels (single or multi-source)
Shortest number of steps in an unweighted grid. Put every source in the queue first; process the queue one level (one "minute", one "step") at a time.
Pseudocode
queue = all source cells; mark them visited steps = 0 while queue not empty: size = queue.size() // freeze the current level repeat size times: cell = queue.poll() if cell is the goal: return steps for each neighbour nb of cell: if inside and open and not visited: mark nb visited // mark when ENQUEUED queue.offer(nb) steps = steps + 1 return -1 // goal unreachable
Multi-source BFS β€” levels grow outward from every source simultaneously
S = source Β· number = BFS level S 1 2 3 3 1 2 3 3 2 2 3 3 2 1 3 3 2 1 S each cell gets the distance to its NEAREST source CELLS IN EACH BFS LEVEL L0 (0,0) (3,4) L1 (0,1) (1,0) (2,4) (3,3) L2 (0,2) (1,1) (1,4) (2,0) (2,3) (3,2) L3 (0,3) (0,4) (1,2) (1,3) (2,1) (2,2) (3,0) (3,1) size = queue.size() freezes one level before expanding it
Template 3 β€” Reverse search from the targets
When the question is "which cells can reach X?", start at X and follow the movement rule backwards. Every cell you reach is an answer.
Pseudocode
reach = boolean[rows][cols] for each target cell t: // e.g. every border cell dfs(t) function dfs(cell): if reach[cell]: return reach[cell] = true for each neighbour nb: if inside and canMove(nb -> cell): // the rule, inverted dfs(nb)
Forward search repeats work; reverse search shares it
FORWARD: one search per start cell mΒ·n searches Γ— O(mΒ·n) each = O((mΒ·n)Β²) REVERSE: one search from all targets T T T T T T T T T T T T seed all T cells, walk the rule backwards: O(mΒ·n)
Template 4 β€” Boundary-shrinking simulation
Fixed traversal orders (spiral, layer-by-layer rotation) are not searches. Keep four walls, walk along them, and pull each wall inward after use.
Pseudocode
top = 0, bottom = rows - 1, left = 0, right = cols - 1 while top <= bottom and left <= right: walk top row left..right; top = top + 1 walk right col top..bottom; right = right - 1 if top <= bottom: walk bottom row right..left; bottom = bottom - 1 if left <= right: walk left col bottom..top; left = left + 1
Spiral on a 4Γ—4 matrix β€” outer ring (blue) then inner ring (gold)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 top = 0 bottom = 3 left = 0 right = 3 1. top row β†’ then top++ 2. right col ↓ then right-- 3. bottom row ← then bottom-- 4. left col ↑ then left++ ring 1 done: top=1 bottom=2 left=1 right=2 ring 2 = inner 2Γ—2 (gold), same four legs stop when top > bottom or left > right guards before legs 3 and 4 stop a single row/col being read twice
TemplateAnswersTimeExtra spaceDrills
DFS flood fillcount / size / recolour regionsO(mΒ·n)O(mΒ·n) recursion worst case65, 66, 67
BFS by levelsminimum steps / minutesO(mΒ·n)O(mΒ·n) queue68, 69
Reverse searchwhich cells can reach a target setO(mΒ·n)O(mΒ·n) visited arrays70
Boundary simulationfixed geometric orderO(mΒ·n)O(1) besides output71
04
Drill 65 Β· Grid DFS

Flood Fill

🏷️

Difficulty

Easy

πŸ”—

LeetCode

LC 733 Β· Flood Fill

🧩

Pattern

DFS flood fill (Template 1)

grid dfs in-place

Problem

An image is a 2D array of integers (pixel colours). Starting from pixel (sr, sc), recolour that pixel and every pixel connected to it 4-directionally that has the same original colour to the new color. Return the modified image. This is the paint-bucket tool from any drawing program.

Example
Input: image = [[1,1,1],[1,1,0],[1,0,1]], sr = 1, sc = 1, color = 2 Output: [[2,2,2],[2,2,0],[2,0,1]] // The bottom-right 1 is only diagonally adjacent to the region, so it keeps colour 1.
Key insight: the recursion's guard image[r][c] != original does double duty β€” it stops at cells of a different colour and at cells already painted, because painted cells no longer hold the original colour. No separate visited array is needed… unless the new colour equals the original colour, in which case painted and unpainted cells look identical and the recursion never ends. That one-line early return is the whole trick of this drill.
From brute force to optimal

A brute-force idea is to repeatedly sweep the whole image, painting any original-coloured pixel adjacent to a painted one, until a sweep changes nothing β€” O((mΒ·n)Β²) in the worst case (a long snake-shaped region gains one pixel per sweep). DFS from the start pixel paints the region directly: every pixel is painted at most once and each paint examines four neighbours, so the work is proportional to the image size. BFS works equally well; DFS is simply shorter to write.

Algorithm
  • Remember original = image[sr][sc]. If it already equals color, return immediately.
  • Call dfs(sr, sc): return if out of bounds or the pixel is not original.
  • Paint the pixel color (this is also the visited mark).
  • Recurse down, up, right, left.
Walkthrough
Flood fill from (1,1) β€” order: down, up, right, left
BEFORE (start = (1,1), colour 1 β†’ 2) 1 1 1 1 1 0 1 0 1 dfs AFTER (corner = visit order) 2 4 2 2 2 3 2 5 2 1 0 2 6 0 1 (2,2) stays 1: its neighbours (1,2) and (2,1) are 0, so no 4-way path of 1s reaches it 6 cells painted, each exactly once Diagonal contact does not count β€” only up / down / left / right.
CallCellActionWhy the next calls stop / continue
1(1,1)paint 2down (2,1)=0 stop Β· up (0,1)=1 recurse
2(0,1)paint 2down (1,1) already 2 Β· up out of bounds Β· right (0,2)=1 recurse
3(0,2)paint 2down (1,2)=0 Β· up out Β· right out Β· left (0,1) is 2 β†’ return
4(0,0)paint 2back in (0,1): left (0,0)=1 recurse β†’ down (1,0)=1
5(1,0)paint 2down (2,0)=1 recurse
6(2,0)paint 2all neighbours are out, 0, or already 2 β†’ unwind to (1,1); its right (1,2)=0, left (1,0) is 2 β†’ done
Java β€” FloodFill.java (complete, runnable)
import java.util.Arrays; public class FloodFill { private static final int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; public static int[][] floodFill(int[][] image, int sr, int sc, int color) { int original = image[sr][sc]; if (original == color) return image; // nothing to do β€” and avoids infinite recursion dfs(image, sr, sc, original, color); return image; } private static void dfs(int[][] image, int r, int c, int original, int color) { if (r < 0 || r >= image.length || c < 0 || c >= image[0].length) return; if (image[r][c] != original) return; // wall, or already recoloured image[r][c] = color; // paint = mark visited for (int[] d : DIRS) dfs(image, r + d[0], c + d[1], original, color); } public static void main(String[] args) { int[][] image = {{1, 1, 1}, {1, 1, 0}, {1, 0, 1}}; int[][] result = floodFill(image, 1, 1, 2); System.out.println(Arrays.deepToString(result)); // [[2, 2, 2], [2, 2, 0], [2, 0, 1]] } }
Complexity
ApproachTimeSpaceNotes
Repeated sweepsO((mΒ·n)Β²)O(1)Simple but quadratic on snake-shaped regions
DFS / BFS from startO(mΒ·n)O(mΒ·n)Each pixel painted once; recursion depth or queue can reach the region size
⚠ Common bugs & edge cases:
  • Forgetting if (original == color) return image; β€” with equal colours the guard never trips and the program dies with StackOverflowError.
  • Reading image[sr][sc] inside the recursion as the colour to match β€” after the first paint it has changed. Capture original once, before recursing.
  • Checking image[r][c] before the bounds test β€” ArrayIndexOutOfBoundsException on the first edge pixel.
  • Including diagonals. The problem is 4-directional; diagonal flooding would wrongly paint (2,2) in the example.
  • Missing import java.util.Arrays when printing with Arrays.deepToString (the original drill code had this bug).
05
Drill 66 Β· Grid DFS Β· Components

Number of Islands

🏷️

Difficulty

Medium

🧩

Pattern

Count connected components with DFS

grid dfs connected components

Problem

Given an m Γ— n grid of characters where '1' is land and '0' is water, return the number of islands. An island is a maximal group of land cells connected horizontally or vertically; the grid is surrounded by water.

Example
Input: grid = [["1","1","0","0","0"], ["1","1","0","0","0"], ["0","0","1","0","0"], ["0","0","0","1","1"]] Output: 3
Key insight: the number of islands equals the number of times the outer scan finds land that no earlier DFS has already consumed. Each DFS sinks its whole island, so a later cell of the same island is just water by the time the scan reaches it. Counting components = counting DFS launches.
From brute force to optimal

A naive approach labels each land cell by checking whether it can reach any previously labelled cell β€” a search per cell, O((mΒ·n)Β²). Instead, scan once; on finding unvisited land, increment the counter and flood-fill the island so it can never be counted again. Each cell is visited by the scan once and sunk at most once. Union-Find also works (union each land cell with its right and down land neighbours, count roots) and is useful if cells arrive online, but DFS is the cleanest for a static grid.

Algorithm
  • Scan rows top to bottom, columns left to right.
  • On '1': count++, then sink(r, c).
  • sink returns on out-of-bounds or non-'1'; otherwise sets '0' and recurses in four directions.
  • Return count.
Walkthrough
Scan + sink on the example β€” three DFS launches
row-major scan; each '1' that is still unvisited starts a new island 1 #1 1 0 0 0 1 1 0 0 0 0 0 1 #2 0 0 0 0 0 1 #3 1 count=1 at (0,0): sink 4 cells (0,1),(1,0),(1,1) now '0' β†’ skipped count=2 at (2,2): sink 1 cell count=3 at (3,3): sink (3,3),(3,4) (3,4) already sunk β†’ skipped answer = 3 (2,2) and (3,3) touch only diagonally β†’ separate islands
Java β€” NumberOfIslands.java (complete, runnable)
public class NumberOfIslands { public static int numIslands(char[][] grid) { if (grid == null || grid.length == 0) return 0; int count = 0; for (int r = 0; r < grid.length; r++) { for (int c = 0; c < grid[0].length; c++) { if (grid[r][c] == '1') { // unvisited land = a new island count++; sink(grid, r, c); // erase the whole island } } } return count; } private static void sink(char[][] grid, int r, int c) { if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length) return; if (grid[r][c] != '1') return; grid[r][c] = '0'; // mark visited by sinking sink(grid, r + 1, c); sink(grid, r - 1, c); sink(grid, r, c + 1); sink(grid, r, c - 1); } public static void main(String[] args) { char[][] grid = { {'1', '1', '0', '0', '0'}, {'1', '1', '0', '0', '0'}, {'0', '0', '1', '0', '0'}, {'0', '0', '0', '1', '1'} }; System.out.println(numIslands(grid)); // 3 } }
Interview follow-up: "Don't modify the input." Use a boolean[][] seen instead of overwriting with '0' β€” same complexity, O(mΒ·n) extra space. "The grid is huge and DFS overflows the stack." Switch the inner flood fill to an explicit ArrayDeque (BFS or iterative DFS).
Complexity
ApproachTimeSpaceNotes
Search from every land cellO((mΒ·n)Β²)O(mΒ·n)Repeats the same island many times
Union-FindO(mΒ·n Β· Ξ±(mΒ·n))O(mΒ·n)Good for streaming land additions (LC 305)
Scan + DFS sinkO(mΒ·n)O(mΒ·n)Recursion depth up to mΒ·n for one snake-shaped island
⚠ Common bugs & edge cases:
  • Comparing with the integer 1 instead of the character '1' β€” grid[r][c] == 1 is always false for a char[][].
  • Incrementing the count inside the DFS instead of in the scan β€” you count cells, not islands.
  • Marking visited after recursing instead of before β€” neighbours recurse back into the cell, giving infinite recursion.
  • Assuming a square grid (grid.length used for both dimensions) β€” fails on 4Γ—5 like the example.
  • Empty grid: guard grid.length == 0 before reading grid[0].length.
06
Drill 67 Β· Grid DFS Β· Aggregation

Max Area of Island

🏷️

Difficulty

Medium

🧩

Pattern

DFS that returns a value

grid dfs post-order sum

Problem

Given a binary matrix grid of 0 (water) and 1 (land), an island is a 4-directionally connected group of 1s. Return the area (number of cells) of the largest island, or 0 if there is no land.

Example
Input: grid = [[0,0,1,0,0], [1,1,1,0,0], [0,1,0,0,1], [0,0,0,1,1]] Output: 5 // the L-shaped island (0,2),(1,0),(1,1),(1,2),(2,1); the other has area 3
Key insight: it is Number of Islands with one change β€” instead of a void DFS, make the DFS return how many cells it sank. A cell contributes 1 + area(down) + area(up) + area(right) + area(left); cells that are water, out of bounds, or already sunk contribute 0. The recursion aggregates the size for free, exactly like computing the size of a tree.
From brute force to optimal

You could run Number of Islands and then do a second pass counting cells per island label, but that needs a label array and two passes. Folding the count into the DFS return value is a single pass with no extra structure. A BFS alternative counts the cells it polls from the queue; same O(mΒ·n).

Algorithm
  • Scan every cell; on land, compute area(r, c) and update best.
  • area returns 0 for out-of-bounds / water / visited.
  • Otherwise sink the cell and return 1 + the four recursive areas.
Walkthrough
Two islands β€” DFS return values bubble up to the scan
corner number = order in which that island's DFS sinks the cell 0 0 1 1 0 0 1 5 1 3 1 2 0 0 0 1 4 0 0 1 1 0 0 0 1 3 1 2 scan hits (0,2) first: dfs returns 1+1+1+1+1 = 5 best = max(0, 5) = 5 scan hits (2,4) next: dfs returns 1+1+1 = 3 best = max(5, 3) = 5 answer = 5 each call returns 1 (itself) + the areas its four children found
Java β€” MaxAreaOfIsland.java (complete, runnable)
public class MaxAreaOfIsland { public static int maxAreaOfIsland(int[][] grid) { int best = 0; for (int r = 0; r < grid.length; r++) { for (int c = 0; c < grid[0].length; c++) { if (grid[r][c] == 1) best = Math.max(best, area(grid, r, c)); } } return best; } // Returns the number of land cells reachable from (r, c), sinking them as it goes. private static int area(int[][] grid, int r, int c) { if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length) return 0; if (grid[r][c] != 1) return 0; grid[r][c] = 0; return 1 + area(grid, r + 1, c) + area(grid, r - 1, c) + area(grid, r, c + 1) + area(grid, r, c - 1); } public static void main(String[] args) { int[][] grid = { {0, 0, 1, 0, 0}, {1, 1, 1, 0, 0}, {0, 1, 0, 0, 1}, {0, 0, 0, 1, 1} }; System.out.println(maxAreaOfIsland(grid)); // 5 } }
Complexity
ApproachTimeSpaceNotes
Label islands, then count per labelO(mΒ·n)O(mΒ·n)Two passes + label array
Scan + DFS returning areaO(mΒ·n)O(mΒ·n)Single pass; recursion depth up to the island size
⚠ Common bugs & edge cases:
  • Sinking the cell after the recursive calls β€” the neighbours call back into it and it is counted repeatedly (or recursion never ends).
  • Returning best initialised to -1 or Integer.MIN_VALUE β€” a grid with no land must return 0.
  • Using a static/global counter that is not reset between islands β€” every island after the first reports a cumulative size.
  • Diagonal adjacency again: (2,1) and (3,3) are not connected, so the two islands must not merge.
07
Drill 68 Β· Grid BFS Β· Multi-source

Rotting Oranges

🏷️

Difficulty

Medium

🧩

Pattern

Multi-source BFS by levels (Template 2)

grid bfs multi-source simulation

Problem

Each cell is 0 (empty), 1 (fresh orange) or 2 (rotten orange). Every minute, each fresh orange 4-directionally adjacent to a rotten one becomes rotten. Return the minimum number of minutes until no fresh orange remains, or -1 if some fresh orange can never rot.

Example
Input: grid = [[2,1,1],[1,1,0],[0,1,1]] Output: 4 Input: grid = [[2,1,1],[0,1,1],[1,0,1]] Output: -1 // the orange at (2,0) is walled off
Key insight: "every minute, all rotten oranges spread at once" is exactly BFS level order with all rotten oranges as simultaneous sources. Level k of the BFS = the oranges that rot at minute k. Running a separate BFS from each rotten orange would be both slower and wrong-headed β€” the answer depends on the nearest rotten orange, which multi-source BFS computes directly.
From brute force to optimal

A direct simulation copies the grid every minute and rots neighbours of rotten cells until nothing changes β€” O((mΒ·n)Β²) because there can be mΒ·n minutes of O(mΒ·n) scans. Multi-source BFS touches each cell once: enqueue every initially rotten orange, count the fresh ones, then process the queue level by level, decrementing fresh each time an orange rots. When the queue empties, fresh == 0 means success.

Algorithm
  • Enqueue all 2s; count all 1s into fresh.
  • While the queue is non-empty and fresh > 0: process exactly queue.size() cells (one minute), rotting fresh neighbours as they are enqueued; then minutes++.
  • Return minutes if fresh == 0, else -1.
Walkthrough
Minute-by-minute BFS levels on the example
minute 0 2 1 1 1 1 1 1 fresh = 6 queue: (0,0) minute 1 2 2 1 2 1 1 1 fresh = 4 queue: (0,1) (1,0) minute 2 2 2 2 2 2 1 1 fresh = 2 queue: (0,2) (1,1) minute 3 2 2 2 2 2 2 1 fresh = 1 queue: (2,1) minute 4 2 2 2 2 2 2 2 fresh = 0 queue: (2,2) red = rotten before this minute Β· gold = rotted during this minute Β· green = fresh
MinutePolled (rotten)Newly rottenfresh left
0 β†’ 1(0,0)(0,1), (1,0)4
1 β†’ 2(0,1), (1,0)(0,2), (1,1)2
2 β†’ 3(0,2), (1,1)(2,1)1
3 β†’ 4(2,1)(2,2)0 β†’ loop stops, return 4
Java β€” RottingOranges.java (complete, runnable)
import java.util.ArrayDeque; import java.util.Queue; public class RottingOranges { private static final int[][] DIRS = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; public static int orangesRotting(int[][] grid) { int rows = grid.length, cols = grid[0].length; Queue<int[]> queue = new ArrayDeque<>(); int fresh = 0; for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { if (grid[r][c] == 2) queue.offer(new int[]{r, c}); // every rotten orange is a source else if (grid[r][c] == 1) fresh++; } } int minutes = 0; while (!queue.isEmpty() && fresh > 0) { int size = queue.size(); // one BFS level = one minute for (int i = 0; i < size; i++) { int[] cell = queue.poll(); for (int[] d : DIRS) { int r = cell[0] + d[0], c = cell[1] + d[1]; if (r >= 0 && r < rows && c >= 0 && c < cols && grid[r][c] == 1) { grid[r][c] = 2; // mark when enqueued, not when polled fresh--; queue.offer(new int[]{r, c}); } } } minutes++; } return fresh == 0 ? minutes : -1; } public static void main(String[] args) { int[][] grid = {{2, 1, 1}, {1, 1, 0}, {0, 1, 1}}; System.out.println(orangesRotting(grid)); // 4 int[][] blocked = {{2, 1, 1}, {0, 1, 1}, {1, 0, 1}}; System.out.println(orangesRotting(blocked)); // -1 } }
Why the fresh > 0 condition matters: without it, the loop runs one extra level for the last batch of oranges (which have no fresh neighbours) and returns 5 instead of 4. The alternative fix is to return max(0, minutes - 1), but stopping when nothing is fresh is clearer.
Complexity
ApproachTimeSpaceNotes
Copy-and-scan simulationO((mΒ·n)Β²)O(mΒ·n)One full scan per minute
Multi-source BFSO(mΒ·n)O(mΒ·n)Each cell enqueued at most once
⚠ Common bugs & edge cases:
  • Off-by-one on minutes (returning 5 here) β€” see the tip above.
  • No fresh oranges at all (e.g. [[0,2]]) must return 0, not -1; the fresh > 0 loop guard handles it.
  • Rotting a neighbour when it is polled instead of when it is enqueued β€” the same orange can be enqueued twice by two rotten neighbours and decrement fresh twice.
  • Running BFS from each rotten orange separately and taking the max β€” gives wrong answers when sources overlap and costs O(kΒ·mΒ·n).
  • Using java.util.Stack or recursion: DFS does not produce minute-by-minute levels.
08
Drill 69 Β· Grid BFS Β· Shortest Path

Shortest Path in Binary Matrix

🏷️

Difficulty

Medium

🧩

Pattern

Single-source BFS, 8 directions

grid bfs shortest path 8-directional

Problem

Given an n Γ— n binary matrix, return the length of the shortest clear path from the top-left cell to the bottom-right cell, or -1 if none exists. A clear path visits only 0 cells, and consecutive cells may be connected in any of the 8 directions (edges or corners). The length is the number of cells visited, including both ends.

Example
Input: grid = [[0,1,0,0], [0,1,0,1], [0,0,0,1], [1,1,0,0]] Output: 5 // (0,0) β†’ (1,0) β†’ (2,1) β†’ (3,2) β†’ (3,3)
Key insight: all moves cost the same (one cell), so the graph is unweighted and BFS's first arrival at the goal is optimal. The only twists are counting cells instead of moves (start the counter at 1) and using an 8-entry direction array.
From brute force to optimal

DFS that tries every path and keeps the minimum is exponential (the number of simple paths in a grid explodes). Dijkstra would work but its priority queue is unnecessary when every edge weight is 1 β€” a FIFO queue already pops nodes in non-decreasing distance order. BFS by levels returns the level number the moment the goal is polled.

Algorithm
  • If the start or the goal is blocked, return -1 immediately.
  • Enqueue (0,0), mark it seen, length = 1.
  • Process level by level; return length when (n-1, n-1) is polled.
  • For each of 8 neighbours that is inside, open and unseen: mark seen, enqueue.
  • After each level, length++. If the queue empties, return -1.
Walkthrough
BFS levels on the 4Γ—4 example
number = path length Β· β–  = blocked 1 β–  5 5 2 β–  4 β–  3 3 4 β–  β–  β–  4 5 QUEUE BY LEVEL L1 (0,0) L2 (1,0) L3 (2,0) (2,1) L4 (1,2) (2,2) (3,2) L5 (0,2) (0,3) (3,3) ← goal diagonal moves (1,0)β†’(2,1)β†’(3,2) make the path 5 cells, not 7 gold arrows = one shortest path
Java β€” ShortestPathBinaryMatrix.java (complete, runnable)
import java.util.ArrayDeque; import java.util.Queue; public class ShortestPathBinaryMatrix { private static final int[][] DIRS = { {-1, -1}, {-1, 0}, {-1, 1}, { 0, -1}, { 0, 1}, { 1, -1}, { 1, 0}, { 1, 1} }; public static int shortestPathBinaryMatrix(int[][] grid) { int n = grid.length; if (grid[0][0] != 0 || grid[n - 1][n - 1] != 0) return -1; boolean[][] seen = new boolean[n][n]; Queue<int[]> queue = new ArrayDeque<>(); queue.offer(new int[]{0, 0}); seen[0][0] = true; int length = 1; // path length counts cells, so start = 1 while (!queue.isEmpty()) { int size = queue.size(); for (int i = 0; i < size; i++) { int[] cell = queue.poll(); if (cell[0] == n - 1 && cell[1] == n - 1) return length; for (int[] d : DIRS) { int r = cell[0] + d[0], c = cell[1] + d[1]; if (r >= 0 && r < n && c >= 0 && c < n && grid[r][c] == 0 && !seen[r][c]) { seen[r][c] = true; queue.offer(new int[]{r, c}); } } } length++; } return -1; } public static void main(String[] args) { int[][] grid = { {0, 1, 0, 0}, {0, 1, 0, 1}, {0, 0, 0, 1}, {1, 1, 0, 0} }; System.out.println(shortestPathBinaryMatrix(grid)); // 5 System.out.println(shortestPathBinaryMatrix(new int[][]{{0, 0, 0}, {1, 1, 0}, {1, 1, 0}})); // 4 } }
Complexity
ApproachTimeSpaceNotes
DFS over all pathsexponentialO(nΒ²)Must explore every path to know the minimum
DijkstraO(nΒ² log n)O(nΒ²)Correct but the heap is wasted on unit weights
BFS by levelsO(nΒ²)O(nΒ²)8 neighbours per cell is still a constant
⚠ Common bugs & edge cases:
  • Not checking grid[n-1][n-1] β€” a blocked goal is only discovered after a full BFS (still correct, but wasteful); a blocked start must be rejected before enqueueing.
  • Returning the number of moves (4 here) instead of cells (5). The single-cell grid [[0]] must return 1.
  • Forgetting the diagonals, or listing {0,0} as a direction (the cell re-enqueues itself).
  • Marking visited when polling rather than when enqueueing β€” cells are enqueued many times and the queue blows up to O(8Β·nΒ²) duplicates.
  • Original drill reused grid to store distances; that works but silently destroys the input β€” this version keeps a separate seen array.
09
Drill 70 Β· Grid Β· Reverse Search

Pacific Atlantic Water Flow

🏷️

Difficulty

Medium

🧩

Pattern

Reverse multi-source DFS (Template 3)

grid dfs reverse search two passes

Problem

An m Γ— n island has a height per cell. The Pacific Ocean touches the top and left edges; the Atlantic touches the bottom and right edges. Rain water flows from a cell to a 4-directional neighbour whose height is less than or equal to its own. Return every cell from which water can reach both oceans.

Example
Input: heights = [[1,2,2,3,5], [3,2,3,4,4], [2,4,5,3,1], [6,7,1,4,5], [5,1,1,2,4]] Output: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]]
Key insight: instead of asking "where can water from this cell go?" for each of the mΒ·n cells, ask the reverse: "which cells can the ocean be reached from?" Start at every ocean-border cell and walk uphill (to neighbours with height β‰₯ current) β€” that is water flow in reverse. Two such searches (one per ocean) mark two boolean maps; the answer is their intersection.
From brute force to optimal

Brute force runs a DFS from every cell, following downhill moves, to see whether it reaches both oceans β€” O(mΒ·n) per cell, O((mΒ·n)Β²) total, with no reuse between overlapping searches. The reverse formulation shares work: each map is filled by one multi-source DFS that visits every cell at most once, so the whole algorithm is O(mΒ·n).

Algorithm
  • Create pacific[m][n] and atlantic[m][n].
  • From every left-column and top-row cell, climb into pacific; from every right-column and bottom-row cell, climb into atlantic.
  • climb(r, c): if already marked, return; mark; recurse into neighbours whose height is >= the current height.
  • Collect cells marked in both maps.
Walkthrough
Two reverse searches and their intersection
PACIFIC-REACHABLE 1 2 2 3 5 3 2 3 4 4 2 4 5 3 1 6 7 1 4 5 5 1 1 2 4 ATLANTIC-REACHABLE 1 2 2 3 5 3 2 3 4 4 2 4 5 3 1 6 7 1 4 5 5 1 1 2 4 BOTH = ANSWER 1 2 2 3 5 3 2 3 4 4 2 4 5 3 1 6 7 1 4 5 5 1 1 2 4 Pacific touches top + left Atlantic touches bottom + right 7 cells: (0,4) (1,3) (1,4) (2,2) (3,0) (3,1) (4,0) each map = one reverse DFS climbing from its border (next height β‰₯ current)
Java β€” PacificAtlantic.java (complete, runnable)
import java.util.ArrayList; import java.util.List; public class PacificAtlantic { private static final int[][] DIRS = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; public static List<List<Integer>> pacificAtlantic(int[][] heights) { List<List<Integer>> result = new ArrayList<>(); if (heights.length == 0) return result; int m = heights.length, n = heights[0].length; boolean[][] pacific = new boolean[m][n]; boolean[][] atlantic = new boolean[m][n]; for (int r = 0; r < m; r++) { climb(heights, pacific, r, 0); // left edge -> Pacific climb(heights, atlantic, r, n - 1); // right edge -> Atlantic } for (int c = 0; c < n; c++) { climb(heights, pacific, 0, c); // top edge -> Pacific climb(heights, atlantic, m - 1, c); // bottom edge -> Atlantic } for (int r = 0; r < m; r++) for (int c = 0; c < n; c++) if (pacific[r][c] && atlantic[r][c]) result.add(List.of(r, c)); return result; } // Walk "uphill" from an ocean: water could flow from the neighbour back down to us. private static void climb(int[][] h, boolean[][] reach, int r, int c) { if (reach[r][c]) return; reach[r][c] = true; for (int[] d : DIRS) { int nr = r + d[0], nc = c + d[1]; if (nr < 0 || nr >= h.length || nc < 0 || nc >= h[0].length) continue; if (h[nr][nc] >= h[r][c]) climb(h, reach, nr, nc); } } public static void main(String[] args) { int[][] heights = { {1, 2, 2, 3, 5}, {3, 2, 3, 4, 4}, {2, 4, 5, 3, 1}, {6, 7, 1, 4, 5}, {5, 1, 1, 2, 4} }; System.out.println(pacificAtlantic(heights)); // [[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] } }
Why (2,2) with height 5 qualifies: towards the Pacific it flows 5 β†’ 3 (1,2) β†’ 2 (0,2) β†’ top edge; towards the Atlantic it flows 5 β†’ 3 (2,3) β†’ 1 (2,4) β†’ right edge. Cell (3,2) with height 1 is a basin β€” every neighbour is higher, so water goes nowhere except where it already is, and it touches neither border.
Complexity
ApproachTimeSpaceNotes
DFS from every cellO((mΒ·n)Β²)O(mΒ·n)Repeats overlapping searches
Reverse DFS from both bordersO(mΒ·n)O(mΒ·n)Each map visits a cell at most once
⚠ Common bugs & edge cases:
  • Using > instead of >= when climbing β€” water flows onto equal heights, so flat plateaus must be included.
  • Climbing in the wrong direction (moving to lower neighbours) β€” that simulates forward flow from the ocean, which is meaningless.
  • Mixing up which edges belong to which ocean: Pacific = row 0 and column 0; Atlantic = row m-1 and column n-1. Corner cells (0, n-1) and (m-1, 0) touch both.
  • The original drill's 3Γ—5 example claimed 4 answer cells; the correct output for that input is 7 cells β€” always re-run examples you copy.
  • Sharing one visited array between both oceans β€” the second search stops early on cells the first one marked.
10
Drill 71 Β· Matrix Simulation

Spiral Matrix

🏷️

Difficulty

Medium

πŸ”—

LeetCode

LC 54 Β· Spiral Matrix

🧩

Pattern

Boundary-shrinking simulation (Template 4)

matrix simulation boundaries

Problem

Given an m Γ— n matrix, return all of its elements in spiral order: left-to-right along the top row, top-to-bottom down the right column, right-to-left along the bottom row, bottom-to-top up the left column, then repeat on the inner sub-matrix.

Example
Input: matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]] Output: [1,2,3,4,8,12,11,10,9,5,6,7] Input: matrix = [[1],[2],[3]] Output: [1,2,3]
Key insight: the spiral is four straight walks repeated. Keep the four boundaries of the unvisited rectangle β€” top, bottom, left, right β€” and after walking along one, move it inward. The only subtlety: after the first two legs the rectangle may have collapsed to a single row or column, so legs three and four must re-check that something is left.
From brute force to optimal

An alternative is a direction-turning walk with a visited matrix: move in the current direction and turn right when the next cell is out of bounds or visited. It is O(mΒ·n) time but O(mΒ·n) extra space. The boundary method needs only four integers. Both visit each element exactly once.

Algorithm
  • Initialise top=0, bottom=m-1, left=0, right=n-1.
  • While top <= bottom && left <= right: walk top row then top++; walk right column then right--.
  • If top <= bottom: walk bottom row backwards then bottom--.
  • If left <= right: walk left column upwards then left++.
Walkthrough
Spiral traversal of a 3Γ—4 matrix
numbers = matrix values; arrows = output order 1 2 3 4 5 6 7 8 9 10 11 12 LEG EMITS β†’ top row 0, cols 0..3 1 2 3 4 ↓ right col 3, rows 1..2 8 12 ← bottom row 2, cols 2..0 11 10 9 ↑ left col 0, rows 1..1 5 β†’ top row 1, cols 1..2 6 7 remaining legs: empty or guarded output: [1,2,3,4,8,12,11,10,9,5,6,7]
RoundtopbottomleftrightEmitted
start0203β€”
after ring 111121 2 3 4 Β· 8 12 Β· 11 10 9 Β· 5
after ring 221216 7 (top leg) Β· right leg rows 2..1 is empty Β· bottom leg skipped (top > bottom) Β· left leg rows 1..2 is empty
loop testβ€”β€”β€”β€”top > bottom β†’ stop
Java β€” SpiralMatrix.java (complete, runnable)
import java.util.ArrayList; import java.util.List; public class SpiralMatrix { public static List<Integer> spiralOrder(int[][] matrix) { List<Integer> out = new ArrayList<>(); if (matrix.length == 0 || matrix[0].length == 0) return out; int top = 0, bottom = matrix.length - 1; int left = 0, right = matrix[0].length - 1; while (top <= bottom && left <= right) { for (int c = left; c <= right; c++) out.add(matrix[top][c]); // β†’ along top top++; for (int r = top; r <= bottom; r++) out.add(matrix[r][right]); // ↓ down right right--; if (top <= bottom) { // a row is left for (int c = right; c >= left; c--) out.add(matrix[bottom][c]); // ← along bottom bottom--; } if (left <= right) { // a column is left for (int r = bottom; r >= top; r--) out.add(matrix[r][left]); // ↑ up left left++; } } return out; } public static void main(String[] args) { int[][] matrix = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}}; System.out.println(spiralOrder(matrix)); // [1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7] System.out.println(spiralOrder(new int[][]{{1}, {2}, {3}})); // [1, 2, 3] } }
Complexity
ApproachTimeSpaceNotes
Turn-right walk + visited[][]O(mΒ·n)O(mΒ·n)Easy to reason about, extra matrix
Boundary shrinkingO(mΒ·n)O(1) extraOutput list not counted
⚠ Common bugs & edge cases:
  • Dropping the if (top <= bottom) / if (left <= right) guards β€” a single row like [[1,2,3]] outputs [1,2,3,2,1].
  • Only testing square matrices. 3Γ—4, 4Γ—3, 1Γ—n and nΓ—1 each exercise a different guard.
  • Reading matrix[0].length on an empty matrix β€” guard for matrix.length == 0 first (missing in the original drill).
  • Mixing up which boundary moves after each leg; write the four legs in a fixed order and update immediately after each.
11
Chapter Eleven Β· Summary

Grids & Matrices at a Glance

Every drill on this page is O(mΒ·n) time β€” the difference is which traversal and what you store per cell. If you can explain why each row in the table uses the technique it does, you have the chapter.

DrillProblemPatternTimeSpaceDifficulty
65Flood FillDFS flood fillO(mΒ·n)O(mΒ·n)Easy
66Number of IslandsDFS component countO(mΒ·n)O(mΒ·n)Medium
67Max Area of IslandDFS returning sizeO(mΒ·n)O(mΒ·n)Medium
68Rotting OrangesMulti-source BFSO(mΒ·n)O(mΒ·n)Medium
69Shortest Path in Binary MatrixBFS, 8 directionsO(nΒ²)O(nΒ²)Medium
70Pacific Atlantic Water FlowReverse DFS from bordersO(mΒ·n)O(mΒ·n)Medium
71Spiral MatrixBoundary simulationO(mΒ·n)O(1) extraMedium
Self-test before moving on:
  • Write the 4-direction DFS guard (bounds β†’ value β†’ mark β†’ recurse) from memory without looking.
  • Explain why BFS needs size = queue.size() to count minutes/steps, and what breaks without it.
  • State the two reasons to mark a cell visited when it is enqueued rather than when it is polled.
  • Re-solve Number of Islands iteratively with an ArrayDeque to remove the recursion-depth risk.
  • Name a grid problem where reverse search beats forward search (hint: Surrounded Regions, LC 130).
What to practice next
➑️

Next page

08 Β· Graphs β€” the same DFS/BFS on explicit adjacency lists, plus Union-Find, topological sort, Kruskal and Dijkstra.

πŸ“˜

Theory refresh

← 06 Β· Binary Trees & BST All Coding Drills 08 Β· Graphs β†’