Seven Java grid drills (65β71) β flood fill, islands, rotting oranges, shortest path, Pacific Atlantic and spiral matrix β with traced diagrams, complete solutions and complexity analysis.
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.
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
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 < colsbefore 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
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
functiondfs(grid, r, c):
if r < 0or r >= rows or c < 0or c >= cols: return// 1. boundsif grid[r][c] != TARGET: return// 2. wrong value / visited
grid[r][c] = VISITED // 3. mark BEFORE recursingforeach (dr, dc) in DIRS:
dfs(grid, r + dr, c + dc) // 4. explore neighboursforeachcell (r, c):
if grid[r][c] == TARGET:
components = components + 1// new region founddfs(grid, r, c)
DFS goes deep first β the call stack is as tall as the path it is following
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 = 0while queue not empty:
size = queue.size() // freeze the current levelrepeat size times:
cell = queue.poll()
if cell is the goal: return steps
foreach neighbour nb of cell:
if inside and open andnot visited:
mark nb visited // mark when ENQUEUED
queue.offer(nb)
steps = steps + 1return -1// goal unreachable
Multi-source BFS β levels grow outward from every source simultaneously
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]
foreach target cell t: // e.g. every border celldfs(t)
functiondfs(cell):
if reach[cell]: return
reach[cell] = trueforeach neighbour nb:
if inside andcanMove(nb -> cell): // the rule, inverteddfs(nb)
Forward search repeats work; reverse search shares it
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 - 1while top <= bottom and left <= right:
walk top row left..right; top = top + 1
walk right col top..bottom; right = right - 1if top <= bottom: walk bottom row right..left; bottom = bottom - 1if left <= right: walk left col bottom..top; left = left + 1
Spiral on a 4Γ4 matrix β outer ring (blue) then inner ring (gold)
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 = 2Output: [[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
Call
Cell
Action
Why the next calls stop / continue
1
(1,1)
paint 2
down (2,1)=0 stop Β· up (0,1)=1 recurse
2
(0,1)
paint 2
down (1,1) already 2 Β· up out of bounds Β· right (0,2)=1 recurse
3
(0,2)
paint 2
down (1,2)=0 Β· up out Β· right out Β· left (0,1) is 2 β return
4
(0,0)
paint 2
back in (0,1): left (0,0)=1 recurse β down (1,0)=1
5
(1,0)
paint 2
down (2,0)=1 recurse
6
(2,0)
paint 2
all 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;
publicclassFloodFill {
privatestaticfinalint[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
publicstaticint[][] 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 recursiondfs(image, sr, sc, original, color);
return image;
}
privatestaticvoiddfs(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 visitedfor (int[] d : DIRS) dfs(image, r + d[0], c + d[1], original, color);
}
publicstaticvoidmain(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
Approach
Time
Space
Notes
Repeated sweeps
O((mΒ·n)Β²)
O(1)
Simple but quadratic on snake-shaped regions
DFS / BFS from start
O(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).
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.
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.
publicclassNumberOfIslands {
publicstaticintnumIslands(char[][] grid) {
if (grid == null || grid.length == 0) return0;
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;
}
privatestaticvoidsink(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 sinkingsink(grid, r + 1, c);
sink(grid, r - 1, c);
sink(grid, r, c + 1);
sink(grid, r, c - 1);
}
publicstaticvoidmain(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
Approach
Time
Space
Notes
Search from every land cell
O((mΒ·n)Β²)
O(mΒ·n)
Repeats the same island many times
Union-Find
O(mΒ·n Β· Ξ±(mΒ·n))
O(mΒ·n)
Good for streaming land additions (LC 305)
Scan + DFS sink
O(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.
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
publicclassMaxAreaOfIsland {
publicstaticintmaxAreaOfIsland(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.privatestaticintarea(int[][] grid, int r, int c) {
if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length) return0;
if (grid[r][c] != 1) return0;
grid[r][c] = 0;
return1 + area(grid, r + 1, c) + area(grid, r - 1, c)
+ area(grid, r, c + 1) + area(grid, r, c - 1);
}
publicstaticvoidmain(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
Approach
Time
Space
Notes
Label islands, then count per label
O(mΒ·n)
O(mΒ·n)
Two passes + label array
Scan + DFS returning area
O(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.
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:4Input: 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 andfresh > 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
Polled (rotten)
Newly rotten
fresh 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;
publicclassRottingOranges {
privatestaticfinalint[][] DIRS = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
publicstaticintorangesRotting(int[][] grid) {
int rows = grid.length, cols = grid[0].length;
Queue<int[]> queue = newArrayDeque<>();
int fresh = 0;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (grid[r][c] == 2) queue.offer(newint[]{r, c}); // every rotten orange is a sourceelseif (grid[r][c] == 1) fresh++;
}
}
int minutes = 0;
while (!queue.isEmpty() && fresh > 0) {
int size = queue.size(); // one BFS level = one minutefor (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(newint[]{r, c});
}
}
}
minutes++;
}
return fresh == 0 ? minutes : -1;
}
publicstaticvoidmain(String[] args) {
int[][] grid = {{2, 1, 1}, {1, 1, 0}, {0, 1, 1}};
System.out.println(orangesRotting(grid)); // 4int[][] 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
Approach
Time
Space
Notes
Copy-and-scan simulation
O((mΒ·n)Β²)
O(mΒ·n)
One full scan per minute
Multi-source BFS
O(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.
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.
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.
import java.util.ArrayDeque;
import java.util.Queue;
publicclassShortestPathBinaryMatrix {
privatestaticfinalint[][] DIRS = {
{-1, -1}, {-1, 0}, {-1, 1},
{ 0, -1}, { 0, 1},
{ 1, -1}, { 1, 0}, { 1, 1}
};
publicstaticintshortestPathBinaryMatrix(int[][] grid) {
int n = grid.length;
if (grid[0][0] != 0 || grid[n - 1][n - 1] != 0) return -1;
boolean[][] seen = newboolean[n][n];
Queue<int[]> queue = newArrayDeque<>();
queue.offer(newint[]{0, 0});
seen[0][0] = true;
int length = 1; // path length counts cells, so start = 1while (!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(newint[]{r, c});
}
}
}
length++;
}
return -1;
}
publicstaticvoidmain(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)); // 5System.out.println(shortestPathBinaryMatrix(newint[][]{{0, 0, 0}, {1, 1, 0}, {1, 1, 0}})); // 4
}
}
Complexity
Approach
Time
Space
Notes
DFS over all paths
exponential
O(nΒ²)
Must explore every path to know the minimum
Dijkstra
O(nΒ² log n)
O(nΒ²)
Correct but the heap is wasted on unit weights
BFS by levels
O(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.
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.
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.
import java.util.ArrayList;
import java.util.List;
publicclassPacificAtlantic {
privatestaticfinalint[][] DIRS = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
publicstaticList<List<Integer>> pacificAtlantic(int[][] heights) {
List<List<Integer>> result = newArrayList<>();
if (heights.length == 0) return result;
int m = heights.length, n = heights[0].length;
boolean[][] pacific = newboolean[m][n];
boolean[][] atlantic = newboolean[m][n];
for (int r = 0; r < m; r++) {
climb(heights, pacific, r, 0); // left edge -> Pacificclimb(heights, atlantic, r, n - 1); // right edge -> Atlantic
}
for (int c = 0; c < n; c++) {
climb(heights, pacific, 0, c); // top edge -> Pacificclimb(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.privatestaticvoidclimb(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);
}
}
publicstaticvoidmain(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
Approach
Time
Space
Notes
DFS from every cell
O((mΒ·n)Β²)
O(mΒ·n)
Repeats overlapping searches
Reverse DFS from both borders
O(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.
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.
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
Round
top
bottom
left
right
Emitted
start
0
2
0
3
β
after ring 1
1
1
1
2
1 2 3 4 Β· 8 12 Β· 11 10 9 Β· 5
after ring 2
2
1
2
1
6 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;
publicclassSpiralMatrix {
publicstaticList<Integer> spiralOrder(int[][] matrix) {
List<Integer> out = newArrayList<>();
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 leftfor (int c = right; c >= left; c--) out.add(matrix[bottom][c]); // β along bottom
bottom--;
}
if (left <= right) { // a column is leftfor (int r = bottom; r >= top; r--) out.add(matrix[r][left]); // β up left
left++;
}
}
return out;
}
publicstaticvoidmain(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(newint[][]{{1}, {2}, {3}})); // [1, 2, 3]
}
}
Complexity
Approach
Time
Space
Notes
Turn-right walk + visited[][]
O(mΒ·n)
O(mΒ·n)
Easy to reason about, extra matrix
Boundary shrinking
O(mΒ·n)
O(1) extra
Output 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.