DSA · Pattern Deep Dive

Matrix & 2D Grid Problems: Traversal, Rotation, Islands & DP

Grid questions look varied but reuse a handful of moves: boundary shrinking, transpose-and-reverse, staircase search, flood fill, BFS layers and row-by-row DP. Here's each one with code.

✍️ Pranjal Jain, Ex-Microsoft · IIT Kanpur 📅 10 October 2026 ⏱ 18 min read
A 2D grid with a spiral traversal path and island clusters
TL;DR
  • Most grid code uses the same toolkit: a direction array, a bounds check, and a visited marker (separate array or in-place).
  • Spiral: four shrinking boundaries. Rotate 90° clockwise: transpose, then reverse each row.
  • Search: fully sorted matrix → 1D binary search; rows and columns sorted separately → staircase from the top-right, O(m + n).
  • Islands/flood fill: DFS or BFS from each unvisited land cell. Shortest steps: BFS, multi-source if there are many starts.
  • Grid DP: right/down movement means fill row by row; one 1D array is usually enough.

Matrix questions are a large share of what shows up in coding rounds, from "print this matrix in spiral order" in a service-company test to "rotting oranges" or "minimum path sum" at Amazon or Flipkart. They feel different from one another, but they're built from a small set of reusable moves. Most bugs in grid code aren't conceptual. They're index errors: swapped rows and columns, missing bounds checks, a boundary that shrinks one step too far.

This guide groups the common problems by the move they use, gives Python and Java code for each, and points out the specific index traps. The final section is a checklist for presenting grid solutions in an interview.

The Grid Toolkit: Directions, Bounds and Visited Cells

Write these once and reuse them in every grid problem:

Python
DIRS = [(1, 0), (-1, 0), (0, 1), (0, -1)]          # down, up, right, left
# add (1,1), (1,-1), (-1,1), (-1,-1) for 8-directional problems

def neighbours(r, c, R, C):
    for dr, dc in DIRS:
        nr, nc = r + dr, c + dc
        if 0 <= nr < R and 0 <= nc < C:
            yield nr, nc
Java
static final int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

static boolean inBounds(int r, int c, int R, int C) {
    return r >= 0 && r < R && c >= 0 && c < C;
}

A few conventions that prevent bugs:

  • Name things R and C (or m and n) and never mix them. matrix[r][c] with r in [0, R) and c in [0, C). Most "works on square matrices, fails on rectangles" bugs come from using one length for both.
  • Flatten when useful: cell (r, c) ↔ index r * C + c; back with divmod(i, C). This helps for binary search, union-find and storing cells in a set.
  • Mark visited when you enqueue, not when you dequeue in BFS. Otherwise the same cell can enter the queue many times.
  • Every grid traversal is O(R·C) if each cell is processed a constant number of times. Say this as your complexity.

Spiral Matrix and Other Traversal Orders

Spiral Matrix (LeetCode 54)

Keep four boundaries and peel the matrix layer by layer: top row, right column, bottom row (reversed), left column (reversed), shrinking each boundary as you finish it.

Python
def spiralOrder(matrix):
    res = []
    top, bottom = 0, len(matrix) - 1
    left, right = 0, len(matrix[0]) - 1
    while top <= bottom and left <= right:
        for c in range(left, right + 1):
            res.append(matrix[top][c])
        top += 1
        for r in range(top, bottom + 1):
            res.append(matrix[r][right])
        right -= 1
        if top <= bottom:                       # a row is left to walk back
            for c in range(right, left - 1, -1):
                res.append(matrix[bottom][c])
            bottom -= 1
        if left <= right:                       # a column is left to walk up
            for r in range(bottom, top - 1, -1):
                res.append(matrix[r][left])
            left += 1
    return res
Java
public List<Integer> spiralOrder(int[][] m) {
    List<Integer> res = new ArrayList<>();
    int top = 0, bottom = m.length - 1, left = 0, right = m[0].length - 1;
    while (top <= bottom && left <= right) {
        for (int c = left; c <= right; c++) res.add(m[top][c]);
        top++;
        for (int r = top; r <= bottom; r++) res.add(m[r][right]);
        right--;
        if (top <= bottom) {
            for (int c = right; c >= left; c--) res.add(m[bottom][c]);
            bottom--;
        }
        if (left <= right) {
            for (int r = bottom; r >= top; r--) res.add(m[r][left]);
            left++;
        }
    }
    return res;
}

Dry run on a 3×4 matrix:

1 2 3 4 pass 1: top row 1 2 3 4 5 6 7 8 right col 8 12 9 10 11 12 bottom row 11 10 9 left col 5 pass 2: top row 6 7 (right col empty, guards stop the rest) output: 1 2 3 4 8 12 11 10 9 5 6 7

The two if guards are the whole difficulty. Without them, a matrix with an odd number of rows (like this one) prints the middle row twice, once forwards and once backwards. Spiral Matrix II (59) runs the same loop but writes 1, 2, 3, … into an n×n grid instead of reading.

Diagonal traversal

Cells on the same anti-diagonal share r + c; cells on the same main diagonal share r − c. That fact turns diagonal problems into grouping problems: Diagonal Traverse (498) groups by r + c and reverses alternate groups; Toeplitz Matrix (766) checks that each cell equals its upper-left neighbour; the N-Queens diagonal checks use the same two keys.

Rotate Image: 90 Degrees In Place (LeetCode 48)

Rotating clockwise sends cell (r, c) to (c, n−1−r). That map is the composition of two simple operations: transpose (r, c) → (c, r), then reverse each row (c, r) → (c, n−1−r).

Python
def rotate(matrix):
    n = len(matrix)
    for i in range(n):                       # transpose: swap across the diagonal
        for j in range(i + 1, n):            # j > i, or you'd swap twice
            matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
    for row in matrix:                       # reverse each row
        row.reverse()
Java
public 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)
        for (int l = 0, r = n - 1; l < r; l++, r--) {
            int t = row[l]; row[l] = row[r]; row[r] = t;
        }
}
1 2 3 transpose 1 4 7 reverse rows 7 4 1 4 5 6 ---------> 2 5 8 ------------> 8 5 2 7 8 9 3 6 9 9 6 3
RotationRecipe
90° clockwiseTranspose, then reverse each row
90° counterclockwiseTranspose, then reverse each column (or reverse rows first, then transpose)
180°Reverse each row, then reverse the order of rows

The alternative is rotating four cells at a time, layer by layer, which is a single pass but much easier to get wrong under pressure. If the interviewer asks for it, the four cells are (i, j), (j, n−1−i), (n−1−i, n−1−j) and (n−1−j, i), for layers i < n/2 and j from i to n−2−i. Rotation in place only works for square matrices; a rectangular matrix needs a new R×C → C×R array.

Set Matrix Zeroes with O(1) Extra Space (LeetCode 73)

If a cell is 0, zero its entire row and column. The trap: zeroing as you scan creates new zeros that then wipe out more rows. The O(R + C) fix records zero rows and columns in two sets. The O(1) fix uses the first row and first column as those sets, with two flags remembering whether the first row and column themselves need zeroing.

Python
def setZeroes(M):
    R, C = len(M), len(M[0])
    first_row = any(M[0][c] == 0 for c in range(C))
    first_col = any(M[r][0] == 0 for r in range(R))
    for r in range(1, R):                    # record markers
        for c in range(1, C):
            if M[r][c] == 0:
                M[r][0] = 0
                M[0][c] = 0
    for r in range(1, R):                    # apply markers to the interior
        for c in range(1, C):
            if M[r][0] == 0 or M[0][c] == 0:
                M[r][c] = 0
    if first_row:
        for c in range(C):
            M[0][c] = 0
    if first_col:
        for r in range(R):
            M[r][0] = 0

The order matters: the interior is updated using the markers before the first row and column are overwritten. It's a good example of a problem where the interviewer is testing whether you can reuse the input as scratch space safely.

Searching a Sorted 2D Matrix

Fully sorted matrix (LeetCode 74): one binary search

Rows are sorted and each row starts after the previous one ends, so reading row by row gives one sorted array of R·C elements. Binary search over indices and convert with divmod.

Python
def searchMatrix(M, target):
    R, C = len(M), len(M[0])
    lo, hi = 0, R * C - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        v = M[mid // C][mid % C]            # flatten index -> (row, col)
        if v == target:
            return True
        if v < target:
            lo = mid + 1
        else:
            hi = mid - 1
    return False

O(log(R·C)). Note it's mid // C and mid % C, dividing by the number of columns. Using R here is a classic bug that passes on square test cases.

Rows and columns sorted independently (LeetCode 240): staircase search

Start at the top-right corner. It's the largest in its row and smallest in its column, so every comparison eliminates a whole row or column.

Java
public boolean searchMatrix(int[][] M, int target) {
    int r = 0, c = M[0].length - 1;
    while (r < M.length && c >= 0) {
        if (M[r][c] == target) return true;
        if (M[r][c] > target) c--;     // everything below in this column is bigger
        else r++;                      // everything left in this row is smaller
    }
    return false;
}

O(R + C). Starting from the top-left doesn't work: both moves (right and down) increase the value, so a comparison doesn't tell you which way to go. This is the same elimination argument as Two Sum II on a sorted array.

Kth Smallest Element in a Sorted Matrix (LeetCode 378)

Binary search on the value, not the index. For a candidate value mid, count cells ≤ mid with the staircase walk in O(n). The smallest value whose count reaches k is the answer.

Python
def kthSmallest(M, k):
    n = len(M)
    lo, hi = M[0][0], M[-1][-1]
    while lo < hi:
        mid = (lo + hi) // 2
        count, c = 0, n - 1
        for r in range(n):                   # staircase count of cells <= mid
            while c >= 0 and M[r][c] > mid:
                c -= 1
            count += c + 1
        if count < k:
            lo = mid + 1
        else:
            hi = mid
    return lo

O(n·log(max − min)). The returned value is guaranteed to be in the matrix because it's the smallest value with count ≥ k. This "binary search on the answer plus a monotone counting function" pattern is explained in depth in binary search on answer problems.

Flood Fill, Number of Islands and Connected Regions

A grid is a graph: cells are nodes, adjacent cells are edges. Connectivity questions are therefore DFS/BFS questions, just without an explicit adjacency list.

Flood Fill (LeetCode 733)

Python
def floodFill(image, sr, sc, color):
    old = image[sr][sc]
    if old == color:                 # otherwise recursion never terminates
        return image
    R, C = len(image), len(image[0])

    def dfs(r, c):
        if not (0 <= r < R and 0 <= c < C) or image[r][c] != old:
            return
        image[r][c] = color          # recolouring doubles as "visited"
        dfs(r + 1, c); dfs(r - 1, c); dfs(r, c + 1); dfs(r, c - 1)

    dfs(sr, sc)
    return image

The early return when the colour is unchanged is the bug interviewers wait for. Without it, recoloured cells still match old and the DFS loops forever.

Number of Islands (LeetCode 200)

Scan every cell; each time you hit unvisited land, count an island and flood it so its cells aren't counted again. Here's the BFS version, which avoids deep recursion:

Java
public int numIslands(char[][] grid) {
    int R = grid.length, C = grid[0].length, islands = 0;
    int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
    Deque<int[]> q = new ArrayDeque<>();
    for (int r = 0; r < R; r++)
        for (int c = 0; c < C; c++) {
            if (grid[r][c] != '1') continue;
            islands++;
            grid[r][c] = '0';                 // mark on enqueue
            q.add(new int[]{r, c});
            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 < R && nc >= 0 && nc < C && grid[nr][nc] == '1') {
                        grid[nr][nc] = '0';
                        q.add(new int[]{nr, nc});
                    }
                }
            }
        }
    return islands;
}

O(R·C) time; the queue holds at most O(min(R, C)) cells in a typical BFS frontier but O(R·C) in the worst case. A recursive DFS on a 1000×1000 all-land grid could recurse up to a million frames deep, which will overflow in Python and likely in Java. The Number of Islands deep dive also covers the union-find approach, which matters for the "islands added one at a time" follow-up (LeetCode 305).

Variations on the same flood

ProblemTwist
Max Area of Island (695)DFS returns the size of the component; take the max
Island Perimeter (463)Each land cell adds 4 minus 2 per shared edge; no traversal needed
Surrounded Regions (130)Flood from border 'O' cells first and mark them safe; flip everything else
Pacific Atlantic Water Flow (417)Reverse the flow: BFS uphill from each ocean's border, intersect the two sets
Number of Closed Islands (1254)Erase border-touching islands first, then count
Number of Distinct Islands (694)Record each island's shape as relative offsets or a path string

"Start from the border and work inward" (Surrounded Regions, Pacific Atlantic, Closed Islands) is the single most useful trick in this family. When a condition is about reaching the edge, reverse the search so it starts from the edge.

Shortest Paths in a Grid with BFS

BFS visits cells in order of distance from the start, so the first time it reaches a cell is via a shortest path, as long as every move costs the same. That's the tool for "minimum steps" questions.

Shortest Path in Binary Matrix (LeetCode 1091)

Python
from collections import deque

def shortestPathBinaryMatrix(grid):
    n = len(grid)
    if grid[0][0] or grid[n - 1][n - 1]:
        return -1
    q = deque([(0, 0, 1)])                   # row, col, path length in cells
    grid[0][0] = 1                           # mark visited
    while q:
        r, c, d = q.popleft()
        if (r, c) == (n - 1, n - 1):
            return d
        for dr in (-1, 0, 1):
            for dc in (-1, 0, 1):            # 8 directions
                nr, nc = r + dr, c + dc
                if 0 <= nr < n and 0 <= nc < n and grid[nr][nc] == 0:
                    grid[nr][nc] = 1
                    q.append((nr, nc, d + 1))
    return -1

Multi-source BFS

When there are many starting points, put them all in the queue at distance 0. BFS then computes each cell's distance to its nearest source in one pass. That solves Rotting Oranges (994) (all rotten oranges start together, the answer is the number of layers), 01 Matrix (542) (distance to nearest 0) and Walls and Gates (286). Running a separate BFS from every source would be O((R·C)²); multi-source is O(R·C). The Rotting Oranges walkthrough traces it layer by layer.

When moves have different costs, BFS no longer works. Costs of 0 or 1 → 0-1 BFS with a deque. General non-negative costs, as in Path With Minimum Effort (1631) → Dijkstra with a heap. Both are in the graph BFS and DFS guide.

Grid Dynamic Programming: Paths, Costs and Squares

When you can only move right and down, there are no cycles, and each cell's answer depends only on the cell above and the cell to the left. Fill the table row by row and the dependencies are always ready.

Minimum Path Sum (LeetCode 64)

Python: one row of state
def minPathSum(grid):
    R, C = len(grid), len(grid[0])
    dp = [float('inf')] * C
    dp[0] = 0
    for r in range(R):
        for c in range(C):
            if c == 0:
                dp[c] += grid[r][c]                       # only from above
            else:
                dp[c] = min(dp[c], dp[c - 1]) + grid[r][c]  # above vs left
    return dp[-1]
Java
public int minPathSum(int[][] g) {
    int R = g.length, C = g[0].length;
    int[] dp = new int[C];
    Arrays.fill(dp, Integer.MAX_VALUE);
    dp[0] = 0;
    for (int r = 0; r < R; r++)
        for (int c = 0; c < C; c++)
            dp[c] = (c == 0 ? dp[c] : Math.min(dp[c], dp[c - 1])) + g[r][c];
    return dp[C - 1];
}

Why does one array work? Before updating, dp[c] still holds the previous row's value (the cell above), and dp[c−1] was just updated for the current row (the cell to the left). In the first row, "above" is infinity, so only the left neighbour counts. O(R·C) time, O(C) space.

Unique Paths II with obstacles (LeetCode 63)

def uniquePathsWithObstacles(grid):
    C = len(grid[0])
    dp = [0] * C
    dp[0] = 1
    for row in grid:
        for c in range(C):
            if row[c] == 1:
                dp[c] = 0                  # no paths through an obstacle
            elif c > 0:
                dp[c] += dp[c - 1]
    return dp[-1]

Maximal Square (LeetCode 221)

Let dp[r][c] be the side of the largest all-'1' square with its bottom-right corner at (r, c). A square of side k there needs squares of side k−1 ending above, to the left and diagonally up-left, so:

dp[r][c] = 1 + min(dp[r−1][c], dp[r][c−1], dp[r−1][c−1]) if the cell is '1', else 0
Java
public int maximalSquare(char[][] M) {
    int R = M.length, C = M[0].length, best = 0;
    int[][] dp = new int[R + 1][C + 1];          // padded: no edge checks
    for (int r = 1; r <= R; r++)
        for (int c = 1; c <= C; c++)
            if (M[r - 1][c - 1] == '1') {
                dp[r][c] = 1 + Math.min(dp[r - 1][c - 1],
                                        Math.min(dp[r - 1][c], dp[r][c - 1]));
                best = Math.max(best, dp[r][c]);
            }
    return best * best;
}

Other grid DPs worth knowing: Triangle (120) (bottom-up is cleanest), Dungeon Game (174) (fill from the bottom-right because the constraint is about minimum health needed going forward), Cherry Pickup (741) (two walkers moving simultaneously, state (step, r1, r2)), and Longest Increasing Path in a Matrix (329) (all four directions, so it's DFS with memoisation rather than row-by-row DP, which only works because strictly increasing paths can't cycle). For submatrix sums, use a 2D prefix sum instead; see prefix sum problems.

Grid Problems in Interviews: Checklist and Common Mistakes

Ask before you code

  • Is the grid rectangular? Can it be empty (R = 0 or C = 0)?
  • Four directions or eight? Can I move diagonally?
  • May I modify the input to mark visited cells?
  • For DP: which moves are allowed? Only right/down, or all four?
  • How large is the grid? (Decides recursion vs. explicit stack.)

Pick the technique

Question shapeTechniqueComplexity
Visit in a specific orderBoundary simulationO(R·C)
Rotate / reflectTranspose + reverseO(n²), O(1) space
Find a value in a sorted gridFlattened binary search or staircaseO(log RC) or O(R + C)
Count or measure regionsDFS/BFS flood fill, union-findO(R·C)
Minimum steps, equal costBFS (multi-source if many starts)O(R·C)
Minimum cost, varying weightsDijkstra or 0-1 BFSO(RC log RC)
Count paths / min cost, right-down onlyRow-by-row DPO(R·C), O(C) space
Rectangle sums2D prefix sumO(1) per query
🚫
Mistakes that fail hidden testsUsing len(matrix) for both dimensions; dividing a flattened index by R instead of C; marking BFS cells visited on dequeue (duplicate work, sometimes TLE); missing the "colour unchanged" check in flood fill; recursive DFS on huge grids; forgetting the spiral guards for odd dimensions; and in Python, creating a 2D array with [[0] * C] * R, which makes R references to the same row.

That last one deserves emphasis because it's silent: [[0] * C] * R looks right, but writing to one row writes to all of them. Use [[0] * C for _ in range(R)].

Practice order

  1. Simulation: 54 Spiral Matrix, 59 Spiral Matrix II, 48 Rotate Image, 73 Set Matrix Zeroes, 498 Diagonal Traverse.
  2. Search: 74 Search a 2D Matrix, 240 Search a 2D Matrix II, 378 Kth Smallest in Sorted Matrix.
  3. Flood fill: 733, 200, 695, 130, 417.
  4. BFS: 1091, 994, 542, 1631.
  5. DP: 62, 63, 64, 221, 120, 329.

Around 25 problems. Once the toolkit at the top of this page is automatic, most new grid problems turn out to be one of these moves with a small twist.

Frequently Asked Questions

How do I traverse a matrix in spiral order?▾
Keep four boundaries: top, bottom, left and right. Walk the top row left to right, then the right column top to bottom, then the bottom row right to left, then the left column bottom to top, shrinking each boundary after you use it. Check that top <= bottom and left <= right again before the third and fourth walks so single rows or columns are not printed twice.
How do you rotate a matrix 90 degrees clockwise in place?▾
Transpose the matrix by swapping matrix[i][j] with matrix[j][i] for j > i, then reverse each row. For counterclockwise, transpose and then reverse each column, or reverse each row first and then transpose. Both approaches are O(n^2) time and O(1) extra space.
What is the fastest way to search a sorted 2D matrix?▾
If each row is sorted and each row starts after the previous row ends, treat the matrix as one sorted array and binary search in O(log(mn)). If rows and columns are sorted independently, start at the top-right corner and move left when the value is too big or down when it is too small, which takes O(m + n).
Should I use DFS or BFS for number of islands?▾
Both visit every cell once and run in O(m x n). DFS is shorter to write recursively, but on a large grid that is one big island the recursion depth can reach m x n and overflow the stack. BFS with a queue or iterative DFS with an explicit stack avoids that, so mention the trade-off in interviews.
When should I use BFS instead of DFS on a grid?▾
Use BFS when you need the shortest path or minimum number of steps in an unweighted grid, because BFS explores cells in increasing distance order. Use DFS or BFS interchangeably for connectivity questions like counting islands. For weighted grids, use Dijkstra or 0-1 BFS.
How do I recognise a grid dynamic programming problem?▾
Movement is restricted to directions that never revisit a cell, usually right and down, and the question asks for a count of paths, a minimum or maximum cost, or the largest shape. Then each cell's answer depends only on already-computed neighbours, so you can fill the table row by row, often with a single 1D array.
Can I modify the input grid to mark visited cells?▾
Often yes, and it saves O(m x n) space, but ask first. Some interviewers consider mutating input a side effect to avoid. If you do mutate, mention that you could restore the grid afterwards or use a separate visited array instead.

Practise Grid and Graph Problems with Prepflix

Prepflix's DSA program covers matrix, BFS/DFS and grid DP patterns with curated problem sets and mock interviews that mirror product-company rounds.

Explore the Prepflix Course →
Pranjal Jain
Pranjal Jain

Ex-Microsoft SDE · IIT Kanpur · Founder of Prepflix. Helps engineers crack startup and product company interviews across India.