Matrix Problems
"A 2D grid is just a graph wearing a rectangular costume — once you see it that way, DFS and BFS unlock everything."
Matrix problems appear in virtually every major tech interview — Google, Amazon, Meta, Microsoft all love them. They test your ability to think spatially, manage 2D state, and apply graph algorithms (DFS/BFS) to a grid structure. Spiral traversal, island counting, BFS distance propagation, in-place transformations, and binary search on sorted matrices are near-guaranteed question types. Mastering the 4 matrix patterns below covers ~90% of all matrix interview questions.
🧼 Introduction to Matrix Problems
Before writing a single line of code, let us build the correct mental
model. A matrix is just a 2D array declared as
int[][] grid with m rows and
n columns. Every cell is identified by its row index
r and column index c.
The single most important insight in this entire lecture is:
Each cell
grid[r][c] is a node. Two cells are
neighbours (connected by an edge) if they share a side (up,
down, left, right). Once you see the matrix as a graph, every graph
algorithm — DFS, BFS, shortest path — instantly applies.
This is the unlock.
📈 The Grid Mental Model
Imagine you are standing on a cell (r, c) in the grid.
You can move in at most 4 directions:
grid[r][c] without verifying
0 ≤ r < m and 0 ≤ c < n. In
DFS, bounds check must be the very first base case —
before checking the cell value. Out-of-bounds access throws
ArrayIndexOutOfBoundsException silently in many LeetCode
environments.
📋 Matrix Anatomy in Java
// Declaration int[][] grid = new int[m][n]; // m rows, n columns, all 0 char[][] board = new char[m][n]; // for island problems (often '0' or '1') // Dimensions int m = grid.length; // number of ROWS int n = grid[0].length; // number of COLUMNS // Access: O(1) grid[r][c]; // row r, column c // Traversal (row-major order): O(m*n) for (int r = 0; r < m; r++) for (int c = 0; c < n; c++) process(grid[r][c]); // Reusable inBounds helper (write this at the top of every grid solution) boolean inBounds(int r, int c) { return r >= 0 && r < m && c >= 0 && c < n; } // 4-direction expansion (use this template ALWAYS) int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}}; for (int[] d : dirs) { int nr = r + d[0], nc = c + d[1]; if (inBounds(nr, nc)) { /* process neighbour */ } }
| Signal Phrase in Problem | Pattern to Apply | Key Algorithm |
|---|---|---|
| "spiral order", "layer by layer", "peel" | Boundary / Spiral Walk | 4-pointer shrink |
| "in place", "O(1) space" + state changes | In-Place State Machine | Sentinel encoding |
| "connected cells", "island", "flood fill", "region" | Grid DFS | Recursive flood fill |
| "shortest distance", "nearest", "minimum time" | Grid BFS | Multi-source BFS |
| "sorted rows and columns", "search in matrix" | 2D Binary Search | Corner walk O(m+n) |
🎯 The 4 Core Matrix Patterns
Pattern 1 — Boundary / Spiral Traversal
When to use: Problems that ask you to visit cells in a specific order that "peels" the matrix from outside in (spiral, layer by layer, zigzag).
Core idea: Maintain 4 boundary pointers —
top, bottom, left,
right — and shrink them inward after traversing
each side. Keep going while top ≤ bottom and
left ≤ right.
List<Integer> spiralOrder(int[][] matrix) { List<Integer> res = new ArrayList<>(); int top = 0, bot = matrix.length - 1; int lft = 0, rgt = matrix[0].length - 1; while (top <= bot && lft <= rgt) { // → traverse top row left-to-right for (int c = lft; c <= rgt; c++) res.add(matrix[top][c]); top++; // ↓ traverse right column top-to-bottom for (int r = top; r <= bot; r++) res.add(matrix[r][rgt]); rgt--; // ← traverse bottom row right-to-left (guard: rows remain) if (top <= bot) { for (int c = rgt; c >= lft; c--) res.add(matrix[bot][c]); bot--; } // ↑ traverse left column bottom-to-top (guard: cols remain) if (lft <= rgt) { for (int r = bot; r >= top; r--) res.add(matrix[r][lft]); lft++; } } return res; } // Time: O(m*n) — every cell visited exactly once // Space: O(1) extra (result array not counted)
if top ≤ bot and
if lft ≤ rgt)?After traversing the top row and right column, the remaining rows/columns might have collapsed. For example, a single-row matrix: top becomes 1 which is > bot=0, so the bottom-row traversal would re-collect cells we already visited. The guards prevent this double-counting.
Pattern 2 — In-Place State Machine
When to use: Problems requiring O(1) extra space where you must record which cells have been "marked" or "changed" without using a separate array.
Core idea: Encode two states in one cell value. Use sentinel values (like -1, 2, or a sign flip) to represent "this cell is scheduled to change" without actually changing it yet. Make changes in a second pass once all decisions are final.
Classic example: Rotate Image (LC 48) — 90° clockwise = Transpose + Reverse each row.
void rotate(int[][] m) { int n = m.length; // Step 1: Transpose — swap across main diagonal (i,j) ↔ (j,i) for (int i = 0; i < n; i++) for (int j = i + 1; j < n; j++) { int tmp = m[i][j]; m[i][j] = m[j][i]; m[j][i] = tmp; } // Step 2: Reverse each row for (int i = 0; i < n; i++) { int lo = 0, hi = n - 1; while (lo < hi) { int tmp = m[i][lo]; m[i][lo] = m[i][hi]; m[i][hi] = tmp; lo++; hi--; } } } // Time: O(n²) | Space: O(1) // Counter-clockwise 90° = Transpose + Reverse each COLUMN // 180° = Reverse all rows + Reverse each row
void setZeroes(int[][] m) { int rows = m.length, cols = m[0].length; boolean row0 = false, col0 = false; // Pass 1: does row 0 / col 0 itself contain a zero? for (int c = 0; c < cols; c++) if (m[0][c] == 0) row0 = true; for (int r = 0; r < rows; r++) if (m[r][0] == 0) col0 = true; // Pass 2: mark row 0 and col 0 as sentinels for inner cells for (int r = 1; r < rows; r++) for (int c = 1; c < cols; c++) if (m[r][c] == 0) { m[r][0] = 0; m[0][c] = 0; } // Pass 3: use sentinels to zero out inner cells for (int r = 1; r < rows; r++) for (int c = 1; c < cols; c++) if (m[r][0] == 0 || m[0][c] == 0) m[r][c] = 0; // Pass 4: zero out row 0 and col 0 based on original flags if (row0) for (int c = 0; c < cols; c++) m[0][c] = 0; if (col0) for (int r = 0; r < rows; r++) m[r][0] = 0; } // Time: O(m*n) | Space: O(1) // KEY: Why handle row0/col0 with separate flags? // Because we reuse them as sentinel storage — if we zeroed them first, // we'd corrupt our own sentinel data.
Pattern 3 — Grid DFS (Flood Fill / Connected Components)
When to use: Count connected components (islands), mark/explore regions, check reachability. Use DFS when you need to visit ALL connected cells and order doesn't matter.
Core idea: Treat the grid as a graph. From any cell, recursively explore all 4 neighbours. Mark cells as "visited" in-place (by changing their value) to avoid revisiting.
int m, n; int numIslands(char[][] grid) { m = grid.length; n = grid[0].length; int count = 0; for (int r = 0; r < m; r++) for (int c = 0; c < n; c++) if (grid[r][c] == '1') { count++; dfs(grid, r, c); } return count; } void dfs(char[][] grid, int r, int c) { // Base cases — ALWAYS bounds check first if (r < 0 || r >= m || c < 0 || c >= n) return; if (grid[r][c] != '1') return; // water or already visited grid[r][c] = '2'; // mark visited in-place (sink it) dfs(grid, r-1, c); // up dfs(grid, r+1, c); // down dfs(grid, r, c-1); // left dfs(grid, r, c+1); // right } // Time: O(m*n) | Space: O(m*n) call stack worst case // Interview follow-up: "What if the grid is huge and DFS stack overflows?" // Answer: Use BFS (iterative queue) — same O(m*n), no stack overflow risk.
Pattern 4 — Grid BFS (Multi-Source Shortest Path)
When to use: Find shortest distance from some source cells to all other cells. BFS guarantees the shortest path in an unweighted graph — this is its defining superpower over DFS.
Multi-source BFS key insight: Instead of running BFS from each source cell one by one (which would be O((m*n)²)), seed the queue with ALL source cells simultaneously. BFS then propagates distances outward level by level in a single O(m*n) pass.
int[][] updateMatrix(int[][] mat) { int m = mat.length, n = mat[0].length; int[][] dist = new int[m][n]; Queue<int[]> q = new LinkedList<>(); // Seed: enqueue ALL 0-cells; mark 1-cells as unvisited (dist=-1) for (int r = 0; r < m; r++) for (int c = 0; c < n; c++) { if (mat[r][c] == 0) { dist[r][c] = 0; q.offer(new int[]{r,c}); } else dist[r][c] = -1; } int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}}; while (!q.isEmpty()) { int[] curr = q.poll(); for (int[] d : dirs) { int nr = curr[0]+d[0], nc = curr[1]+d[1]; if (nr>=0 && nr<m && nc>=0 && nc<n && dist[nr][nc]==-1) { dist[nr][nc] = dist[curr[0]][curr[1]] + 1; q.offer(new int[]{nr, nc}); } } } return dist; } // Time: O(m*n) | Space: O(m*n) // Why dist=-1 and not Integer.MAX_VALUE? Avoids overflow on +1 and // serves as the "unvisited" sentinel naturally.
Pattern 5 — 2D Binary Search (Corner Walk)
When to use: Searching in a matrix where rows are sorted left-to-right AND columns are sorted top-to-bottom. Brute force O(m*n) is too slow; the sorted structure lets us do O(m+n).
Core idea: Start at the top-right corner. This cell has a magical property: it is the maximum of its row and the minimum of its column. So we can make a definitive decision at every step — either eliminate this row (go down) or this column (go left).
boolean searchMatrix(int[][] matrix, int target) { int r = 0, c = matrix[0].length - 1; // top-right corner while (r < matrix.length && c >= 0) { if (matrix[r][c] == target) return true; else if (matrix[r][c] > target) c--; // too large → go left else r++; // too small → go down } return false; } // Time: O(m+n) | Space: O(1) // LC 74 vs LC 240: // LC 74: Fully sorted (row-major), last of row < first of next row // → treat as 1D, use classic binary search: O(log(m*n)) // LC 240: Only row-sorted AND col-sorted independently // → need corner walk: O(m+n) ← this solution
Top-left: both neighbours (right and down) are larger — we can never eliminate anything when target is larger.
Bottom-right: both neighbours (left and up) are smaller — same problem when target is smaller.
Top-right is the sweet spot: left neighbour is smaller (eliminate column if curr is too large), down neighbour is larger (eliminate row if curr is too small). One clear decision at every step.
💪 In-Lecture Practice Problems
Work through these in order. Each card covers one of the 4 core patterns. Try to solve before revealing the solution. The dry runs are your best teacher.
Given an m×n matrix, return all elements in spiral order.
Think of peeling an onion. Traverse the outermost ring (top row → right col ↓ bottom row ← left col ↑), then move the four boundaries inward and repeat. Always guard the bottom and left passes for odd-shaped matrices.
▶ Full Solution + Dry Run
List<Integer> spiralOrder(int[][] mat) { List<Integer> res = new ArrayList<>(); int top=0, bot=mat.length-1, lft=0, rgt=mat[0].length-1; while (top <= bot && lft <= rgt) { for (int c=lft; c<=rgt; c++) res.add(mat[top][c]); top++; for (int r=top; r<=bot; r++) res.add(mat[r][rgt]); rgt--; if (top <= bot) { for (int c=rgt; c>=lft; c--) res.add(mat[bot][c]); bot--; } if (lft <= rgt) { for (int r=bot; r>=top; r--) res.add(mat[r][lft]); lft++; } } return res; }
Rotate an n×n matrix 90° clockwise in place with O(1) extra space.
Transpose the matrix (swap [i][j] with [j][i]), then reverse each row. The mathematical proof: rotating 90° clockwise maps position (r,c) to (c, n-1-r). Transposing gives (c,r), reversing each row turns (c,r) into (c,n-1-r). QED.
▶ Full Solution + Step-by-Step Proof
void rotate(int[][] m) { int n = m.length; for (int i=0; i<n; i++) for (int j=i+1; j<n; j++) { int t=m[i][j]; m[i][j]=m[j][i]; m[j][i]=t; } for (int[] row : m) { int lo=0, hi=n-1; while (lo<hi) { int t=row[lo]; row[lo]=row[hi]; row[hi]=t; lo++; hi--; } } }
If any cell is 0, set its entire row and column to 0. Do it in place using O(1) extra space.
Naive: use a separate boolean array to mark rows/cols → O(m+n) space. Optimised: borrow the first row and first column as your marker arrays. Two boolean flags remember if those borrowed sentinels themselves originally had zeros. 4 passes total, all O(m*n) time, O(1) space.
▶ Full Solution
void setZeroes(int[][] m) { boolean r0=false, c0=false; for (int c=0; c<m[0].length; c++) if (m[0][c]==0) r0=true; for (int r=0; r<m.length; r++) if (m[r][0]==0) c0=true; for (int r=1; r<m.length; r++) for (int c=1; c<m[0].length; c++) if (m[r][c]==0) { m[r][0]=0; m[0][c]=0; } for (int r=1; r<m.length; r++) for (int c=1; c<m[0].length; c++) if (m[r][0]==0||m[0][c]==0) m[r][c]=0; if (r0) for (int c=0; c<m[0].length; c++) m[0][c]=0; if (c0) for (int r=0; r<m.length; r++) m[r][0]=0; }
Given a 2D grid of '1' (land) and '0' (water), count the number of islands (groups of adjacent land cells).
Scan every cell. Each time you find an unvisited '1', you've discovered a new island → increment count. Then DFS to sink the entire island (change all connected '1's to '2' so they are never counted again). This is the Flood Fill pattern — exactly how a paint-bucket tool works.
▶ Full Solution + Dry Run
int numIslands(char[][] g) { int count=0; for (int r=0; r<g.length; r++) for (int c=0; c<g[0].length; c++) if (g[r][c]=='1') { count++; dfs(g,r,c); } return count; } void dfs(char[][] g, int r, int c) { if (r<0||r>=g.length||c<0||c>=g[0].length||g[r][c]!='1') return; g[r][c]='2'; dfs(g,r-1,c); dfs(g,r+1,c); dfs(g,r,c-1); dfs(g,r,c+1); } // Islands at (0,0)-(1,1) → count=1, Island (2,2) → count=2, (3,3) → count=3
Given a binary matrix, return a matrix where each cell = distance to its nearest 0.
Wrong approach: For every 1-cell, BFS to find
nearest 0 → O((m*n)²).
Key insight:
Seed BFS with ALL 0-cells simultaneously. BFS
radiates outward level-by-level. Each 1-cell gets its distance
from whichever 0-source reaches it first — guaranteed to
be the nearest. Single O(m*n) pass.
▶ Full Solution + Trace
int[][] updateMatrix(int[][] mat) { int m=mat.length, n=mat[0].length; int[][] dist = new int[m][n]; Queue<int[]> q = new LinkedList<>(); for (int r=0; r<m; r++) for (int c=0; c<n; c++) { if (mat[r][c]==0) { dist[r][c]=0; q.offer(new int[]{r,c}); } else dist[r][c]=-1; } int[][] dirs={{-1,0},{1,0},{0,-1},{0,1}}; while (!q.isEmpty()) { int[] cur=q.poll(); for (int[] d:dirs) { int nr=cur[0]+d[0], nc=cur[1]+d[1]; if (nr>=0&&nr<m&&nc>=0&&nc<n&&dist[nr][nc]==-1) { dist[nr][nc]=dist[cur[0]][cur[1]]+1; q.offer(new int[]{nr,nc}); } } } return dist; }
Grid cells: 0=empty, 1=fresh orange, 2=rotten. Each minute, rotten oranges spread to adjacent fresh ones. Return minimum minutes to rot all, or -1 if impossible.
Same multi-source BFS as 01 Matrix, but now track BFS levels as minutes. Count fresh oranges upfront. After BFS completes, if fresh > 0 → return -1 (unreachable). Otherwise return minute count.
▶ Full Solution
int orangesRotting(int[][] g) { int m=g.length, n=g[0].length, fresh=0, mins=0; Queue<int[]> q=new LinkedList<>(); for (int r=0;r<m;r++) for (int c=0;c<n;c++) { if (g[r][c]==2) q.offer(new int[]{r,c}); if (g[r][c]==1) fresh++; } if (fresh==0) return 0; int[][] dirs={{-1,0},{1,0},{0,-1},{0,1}}; while (!q.isEmpty()&&fresh>0) { mins++; int sz=q.size(); for (int i=0;i<sz;i++) { int[] cur=q.poll(); for (int[] d:dirs) { int nr=cur[0]+d[0], nc=cur[1]+d[1]; if (nr>=0&&nr<m&&nc>=0&&nc<n&&g[nr][nc]==1) { g[nr][nc]=2; fresh--; q.offer(new int[]{nr,nc}); } } } } return fresh==0 ? mins : -1; }
Integers in each row are sorted left-to-right; integers in each column are sorted top-to-bottom. Search for a target in O(m+n).
Start at top-right corner. This is the max of its row and min of its column. If target < current → go left (eliminate this column). If target > current → go down (eliminate this row). Each step eliminates one row or column → O(m+n) total.
▶ Full Solution + Walk Trace
boolean searchMatrix(int[][] m, int t) { int r=0, c=m[0].length-1; while (r<m.length && c>=0) { if (m[r][c]==t) return true; else if (m[r][c]>t) c--; else r++; } return false; } // Trace target=5: (0,3)=11>5→c=2, (0,2)=7>5→c=1, // (0,1)=4<5→r=1, (1,1)=5==5 → true ✓
Water flows from higher or equal cells to lower. Find all cells that can flow to both Pacific (top/left edges) and Atlantic (bottom/right edges) oceans.
Reverse the flow direction. Instead of asking "where can this cell drain to?", ask "which cells can reach this ocean boundary?". BFS/DFS from Pacific borders going uphill, then BFS/DFS from Atlantic borders going uphill. Cells reachable from BOTH are the answer.
▶ Full Solution
List<List<Integer>> pacificAtlantic(int[][] h) { int m=h.length, n=h[0].length; boolean[][] pac=new boolean[m][n], atl=new boolean[m][n]; Queue<int[]> pq=new LinkedList<>(), aq=new LinkedList<>(); for (int i=0;i<m;i++) { pq.offer(new int[]{i,0}); pac[i][0]=true; aq.offer(new int[]{i,n-1}); atl[i][n-1]=true; } for (int j=0;j<n;j++) { pq.offer(new int[]{0,j}); pac[0][j]=true; aq.offer(new int[]{m-1,j}); atl[m-1][j]=true; } bfs(h, pq, pac, m, n); bfs(h, aq, atl, m, n); List<List<Integer>> res=new ArrayList<>(); for (int r=0;r<m;r++) for (int c=0;c<n;c++) if (pac[r][c]&&atl[r][c]) res.add(Arrays.asList(r,c)); return res; } void bfs(int[][] h, Queue<int[]> q, boolean[][] vis, int m, int n) { int[][] d={{-1,0},{1,0},{0,-1},{0,1}}; while (!q.isEmpty()) { int[] c=q.poll(); for (int[] dir:d) { int nr=c[0]+dir[0], nc=c[1]+dir[1]; if (nr>=0&&nr<m&&nc>=0&&nc<n&&!vis[nr][nc]&&h[nr][nc]>=h[c[0]][c[1]]) { vis[nr][nc]=true; q.offer(new int[]{nr,nc}); } } } }
📝 Assignment — Company Interview Problems
All 4 matrix patterns covered. Problems sorted by company frequency and difficulty. Each problem in the assignment file includes: the LeetCode link, the exact pattern to apply, a step-by-step hint, common mistakes, and the companies that ask it most frequently.
📄 Open Assignment.md →
✅ Lecture Completion Checklist
Check each item before advancing to Lecture 15 (Trees). Be honest — these are your interview readiness criteria.
Trees are the natural progression from 2D grids — both are graphs you traverse with DFS and BFS. Pre/in/post-order traversal = DFS patterns. Level-order = BFS. The grid patterns you just mastered will reappear constantly in tree problems. You are building on solid ground.