Matrix — Worked Examples

Arrays & StringsPriority 4 of 5 — High value — a gap here costs you roundsHigh value Updated Sep 18, 2026
Section priorityPriority 5 of 5 — Must know — expect it in almost every loopMust knowPriority 4 of 5 — High value — a gap here costs you roundsHigh valuePriority 3 of 5 — Worth knowing — usually a variant of a must-know patternWorth knowingPriority 2 of 5 — Niche — read once, revisit only if a company is known to askNicheMarked on the sections that carry it — unmarked sections are background/reference.

Scope — The worked-solution archive behind matrix.md: seventeen problems grouped by the geometry or technique each one turns on — traversal order, in-place transformation, staircase search, grid search, 2D DP, and row-pair compression. See also: matrix.md — the parent sheet: the traversal templates, the index↔coordinate arithmetic and the pattern-selection strategy these solutions apply; dfs.md and bfs.md — grid search in its own right; prefix_sum.md — the 2D prefix sum and row-pair compression theory; dp.md — grid DP; monotonic_stack.md — the histogram step inside LC 85.

LeetCode Problem Lists

Overview

This is the long tail of matrix.md. The parent keeps the templates, the essential matrix properties and the pattern-selection strategy; this file keeps the problems that apply them.

Key Properties

  • Complexity: stated per solution; most are O(m·n), and the ones that are not are the point of the problem
  • Core Idea: almost every matrix problem reduces to a choice of traversal order or a reduction to 1D — the groups below are those choices
  • When to Use: after the parent’s decision tree has named the pattern

Traversal & Diagonals

1) Spiral Matrix — LC 54 Priority 5 of 5 — Must know — expect it in almost every loop

Traverse matrix in spiral order using boundary pointers.

python
# LC 54 - Spiral Matrix
# V0
# IDEA : 4 cases: right, down, left, up + boundary condition
class Solution(object):
    def spiralOrder(self, matrix):
        if not matrix:
            return []
        res = []
        left, right = 0, len(matrix[0]) - 1
        top, bottom = 0, len(matrix) - 1
        while left <= right and top <= bottom:
            # right
            for j in range(left, right + 1):
                res.append(matrix[top][j])
            # down
            for i in range(top + 1, bottom):
                res.append(matrix[i][right])
            # left
            for j in range(left, right + 1)[::-1]:
                if top < bottom:
                    res.append(matrix[bottom][j])
            # up
            for i in range(top + 1, bottom)[::-1]:
                if left < right:
                    res.append(matrix[i][left])
            left += 1
            right -= 1
            top += 1
            bottom -= 1
        return res
java
// LC 54 - Spiral Matrix
// IDEA: Four boundary pointers (left, right, top, bottom); shrink after each direction
// time = O(M*N), space = O(1)
public List<Integer> spiralOrder(int[][] matrix) {
    List<Integer> res = new ArrayList<>();
    int left = 0, right = matrix[0].length - 1, top = 0, bottom = matrix.length - 1;
    while (left <= right && top <= bottom) {
        for (int j = left; j <= right; j++) res.add(matrix[top][j]);
        for (int i = top + 1; i <= bottom; i++) res.add(matrix[i][right]);
        if (top < bottom) for (int j = right - 1; j >= left; j--) res.add(matrix[bottom][j]);
        if (left < right) for (int i = bottom - 1; i > top; i--) res.add(matrix[i][left]);
        left++; right--; top++; bottom--;
    }
    return res;
}

2) Diagonal Traverse — LC 498 — (r+c) % 2 parity

Visit every element in zigzag diagonal order: even-sum diagonals go UP-RIGHT, odd-sum go DOWN-LEFT.

Core Idea:

Every cell (r, c) belongs to a diagonal identified by r + c. The parity of that sum determines travel direction:

  • (r+c) % 2 == 0 → moving UP-RIGHT (r--, c++)
  • (r+c) % 2 == 1 → moving DOWN-LEFT (r++, c--)

Boundary conditions always take priority over the normal move:

Direction Hit which wall Override action
UP-RIGHT right wall (c == n-1) r++ (drop down)
UP-RIGHT top wall (r == 0) c++ (slide right)
DOWN-LEFT bottom wall (r == m-1) c++ (slide right)
DOWN-LEFT left wall (c == 0) r++ (drop down)

Pattern: single for loop over all m*n elements; decide next (r, c) via parity + boundary checks.

java
// LC 498 - Diagonal Traverse
// IDEA: (r+c)%2 parity → even = UP-RIGHT, odd = DOWN-LEFT; boundary checks first
// time = O(M*N), space = O(1)
public int[] findDiagonalOrder(int[][] mat) {
    if (mat == null || mat.length == 0 || mat[0].length == 0) return new int[]{};
    int m = mat.length, n = mat[0].length;
    int[] res = new int[m * n];
    int r = 0, c = 0;
    for (int i = 0; i < res.length; i++) {
        res[i] = mat[r][c];
        if ((r + c) % 2 == 0) {          // UP-RIGHT
            if      (c == n - 1) r++;     // hit right wall → go down
            else if (r == 0)     c++;     // hit top wall   → go right
            else               { r--; c++; }
        } else {                          // DOWN-LEFT
            if      (r == m - 1) c++;     // hit bottom wall → go right
            else if (c == 0)     r++;     // hit left wall   → go down
            else               { r++; c--; }
        }
    }
    return res;
}

Dry-run — mat = [[1,2,3],[4,5,6],[7,8,9]]:

text
Step | (r,c) | val | r+c | direction  | boundary/move
-----|-------|-----|-----|------------|---------------------
  0  | (0,0) |  1  |  0  | UP-RIGHT   | r==0  → c++ (right)
  1  | (0,1) |  2  |  1  | DOWN-LEFT  | c==0? no, r==m-1? no → r++,c--
  2  | (1,0) |  4  |  1  | DOWN-LEFT  | r==m-1? no, c==0 → r++ (down)
  3  | (2,0) |  7  |  2  | UP-RIGHT   | r==m-1? c++ (right)
  4  | (2,1) |  8  |  3  | DOWN-LEFT  | r==m-1 → c++ (right)
  5  | (2,2) |  9  |  4  | UP-RIGHT   | c==n-1 → r++ (but done)

→ output: [1, 2, 4, 7, 5, 3, 6, 8, 9]

Alternative approach — diagonal-by-diagonal (V0-0-1):

java
// Iterate over each diagonal d = 0..m+n-2; set start (r,c) and walk
// time = O(M*N), space = O(1)
public int[] findDiagonalOrder(int[][] mat) {
    int m = mat.length, n = mat[0].length;
    int[] res = new int[m * n];
    int idx = 0;
    for (int d = 0; d < m + n - 1; d++) {
        if (d % 2 == 0) {                       // UP-RIGHT
            int r = Math.min(d, m - 1), c = d - r;
            while (r >= 0 && c < n) { res[idx++] = mat[r--][c++]; }
        } else {                                 // DOWN-LEFT
            int c = Math.min(d, n - 1), r = d - c;
            while (c >= 0 && r < m) { res[idx++] = mat[r++][c--]; }
        }
    }
    return res;
}

Similar LC problems:

Problem LC # Key Technique
Diagonal Traverse 498 (r+c)%2 parity Boundary simulation
Diagonal Traverse II 1424 r+c group key Group by anti-diagonal, collect in order
Sort the Matrix Diagonally 1329 r-c group key Group by main diagonal, sort each
Spiral Matrix 54 boundary pointers Shrink 4 boundaries each full loop
Spiral Matrix II 59 boundary pointers Same spiral, fill values instead
Rotate Image 48 coordinate math Transpose + reverse rows

3) Sort the Matrix Diagonally — LC 1329 — grouping by i - j

Group cells by i - j (same diagonal), sort each group, refill matrix in row-major order.

Core insight: any two cells (i1,j1) and (i2,j2) are on the same top-left→bottom-right diagonal iff i1 - j1 == i2 - j2. Use this as a HashMap key to collect, sort, then rewrite each diagonal.

java
// LC 1329 - Sort the Matrix Diagonally
// IDEA: Group by diagonal key (i-j) → min-heap per diagonal → refill row-major
// time = O(M*N*log(min(M,N))), space = O(M*N)
public int[][] diagonalSort(int[][] mat) {
    int m = mat.length, n = mat[0].length;
    Map<Integer, PriorityQueue<Integer>> map = new HashMap<>();
    // Pass 1: collect each diagonal into a min-heap
    for (int i = 0; i < m; i++)
        for (int j = 0; j < n; j++)
            map.computeIfAbsent(i - j, k -> new PriorityQueue<>()).add(mat[i][j]);
    // Pass 2: refill — row-major order matches diagonal top-to-bottom order
    for (int i = 0; i < m; i++)
        for (int j = 0; j < n; j++)
            mat[i][j] = map.get(i - j).poll();
    return mat;
}

Alternative (sort descending + remove from tail):

java
// Sort list descending, remove from end to get ascending values
for (List<Integer> list : map.values())
    Collections.sort(list, Collections.reverseOrder());
for (int i = 0; i < m; i++)
    for (int j = 0; j < n; j++)
        mat[i][j] = map.get(i - j).remove(map.get(i - j).size() - 1);

Similar Problems using diagonal grouping key:

Problem Key Condition
Toeplitz Matrix (LC 766) i - j All cells in group must equal the first
Diagonal Traverse II (LC 1424) i + j Anti-diagonal grouping; reverse each group
Sort Matrix Diagonally (LC 1329) i - j Sort each group ascending

Transformation & In-Place Modification

4) Rotate Image — LC 48 Priority 5 of 5 — Must know — expect it in almost every loop

Rotate matrix 90° clockwise in-place: Transpose then reverse each row.

python
# LC 48 - Rotate Image
# V0
# IDEA : TRANSPOSE (i,j -> j,i) -> REVERSE each row
class Solution(object):
    def rotate(self, matrix):
        if not matrix:
            return
        l = len(matrix)
        w = len(matrix[0])
        # Step 1: Transpose — swap matrix[i][j] with matrix[j][i]
        for i in range(l):
            for j in range(i + 1, w):
                matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
        # Step 2: Reverse each row
        for i in range(l):
            matrix[i] = matrix[i][::-1]
        return matrix
java
// LC 48 - Rotate Image
// IDEA: Transpose (swap [i][j] with [j][i]) then reverse each row
// time = O(N^2), space = O(1)
public void rotate(int[][] matrix) {
    int n = matrix.length;
    for (int i = 0; i < n; i++)
        for (int j = i + 1; j < n; j++) { int t = matrix[i][j]; matrix[i][j] = matrix[j][i]; matrix[j][i] = t; }
    for (int[] row : matrix) { int l = 0, r = row.length - 1; while (l < r) { int t = row[l]; row[l++] = row[r]; row[r--] = t; } }
}

5) Game of Life — LC 289 — in-place state transition

Simulate next state for all cells simultaneously using 8-neighbor rules.

python
# LC 289 - Game of Life
# V0
# IDEA : copy board, apply all 4 rules using 8-directional neighbors
# Time: O(m*n), Space: O(m*n)
class Solution:
    def gameOfLife(self, board) -> None:
        neighbors = [(1,0),(1,-1),(0,-1),(-1,-1),(-1,0),(-1,1),(0,1),(1,1)]
        rows, cols = len(board), len(board[0])
        copy_board = [[board[r][c] for c in range(cols)] for r in range(rows)]
        for row in range(rows):
            for col in range(cols):
                live_neighbors = sum(
                    copy_board[row + dr][col + dc]
                    for dr, dc in neighbors
                    if 0 <= row + dr < rows and 0 <= col + dc < cols
                )
                # Rule 1 & 3: live cell dies
                if copy_board[row][col] == 1 and (live_neighbors < 2 or live_neighbors > 3):
                    board[row][col] = 0
                # Rule 4: dead cell becomes alive
                elif copy_board[row][col] == 0 and live_neighbors == 3:
                    board[row][col] = 1
java
// LC 289 - Game of Life
// IDEA: Encode next state in same cell: 2 = was dead now alive, -1 = was alive now dead
// time = O(M*N), space = O(1)
public void gameOfLife(int[][] board) {
    int m = board.length, n = board[0].length;
    int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0},{1,1},{1,-1},{-1,1},{-1,-1}};
    for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) {
        int live = 0;
        for (int[] d : dirs) { int r = i+d[0], c = j+d[1]; if (r>=0&&r<m&&c>=0&&c<n&&Math.abs(board[r][c])==1) live++; }
        if (board[i][j] == 1 && (live < 2 || live > 3)) board[i][j] = -1;
        if (board[i][j] == 0 && live == 3) board[i][j] = 2;
    }
    for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) board[i][j] = board[i][j] > 0 ? 1 : 0;
}

6) Set Matrix Zeroes — LC 73 Priority 4 of 5 — High value — a gap here costs you rounds

Mark which rows/columns need zeroing, then apply in two passes.

python
# LC 73 - Set Matrix Zeroes
# V0
# IDEA : collect zero positions first, then set rows/cols to 0
# Time: O(m*n), Space: O(m+n)
class Solution(object):
    def setZeroes(self, matrix):
        if not matrix:
            return
        l, w = len(matrix), len(matrix[0])
        x_zeros = set()  # columns to zero
        y_zeros = set()  # rows to zero
        for i in range(l):
            for j in range(w):
                if matrix[i][j] == 0:
                    x_zeros.add(j)
                    y_zeros.add(i)
        # zero entire rows
        for i in y_zeros:
            matrix[i] = [0] * w
        # zero entire columns
        for j in x_zeros:
            for i in range(l):
                matrix[i][j] = 0
java
// LC 73 - Set Matrix Zeroes
// IDEA: Use first row/col as markers; scan once to mark, once to apply
// time = O(M*N), space = O(1)
public void setZeroes(int[][] matrix) {
    int m = matrix.length, n = matrix[0].length;
    boolean firstRowZero = false, firstColZero = false;
    for (int j = 0; j < n; j++) if (matrix[0][j] == 0) firstRowZero = true;
    for (int i = 0; i < m; i++) if (matrix[i][0] == 0) firstColZero = true;
    for (int i = 1; i < m; i++) for (int j = 1; j < n; j++)
        if (matrix[i][j] == 0) { matrix[i][0] = 0; matrix[0][j] = 0; }
    for (int i = 1; i < m; i++) for (int j = 1; j < n; j++)
        if (matrix[i][0] == 0 || matrix[0][j] == 0) matrix[i][j] = 0;
    if (firstRowZero) Arrays.fill(matrix[0], 0);
    if (firstColZero) for (int i = 0; i < m; i++) matrix[i][0] = 0;
}

7) Image Smoother — LC 661 — 8-directional neighbourhood

python
def imageSmoother(M):
    """
    Smooth image by averaging 8-connected neighbors
    Time: O(m*n), Space: O(m*n)
    """
    if not M or not M[0]:
        return []
    
    rows, cols = len(M), len(M[0])
    result = [[0] * cols for _ in range(rows)]
    
    # 8-directional + current cell
    directions = [(di, dj) for di in [-1, 0, 1] for dj in [-1, 0, 1]]
    
    for i in range(rows):
        for j in range(cols):
            total = 0
            count = 0
            
            for di, dj in directions:
                ni, nj = i + di, j + dj
                if 0 <= ni < rows and 0 <= nj < cols:
                    total += M[ni][nj]
                    count += 1
            
            result[i][j] = total // count
    
    return result

8) Search a 2D Matrix — LC 74 — binary search on a flattened index

Treat the fully sorted matrix as a 1D array and binary search.

python
# LC 74 - Search a 2D Matrix
# V0
# IDEA : BINARY SEARCH — treat matrix as flat sorted array
# Time: O(log(m*n)), Space: O(1)
class Solution(object):
    def searchMatrix(self, matrix, target):
        if not matrix:
            return False
        m, n = len(matrix), len(matrix[0])
        left, right = 0, m * n - 1
        while left <= right:
            mid = (left + right) // 2
            val = matrix[mid // n][mid % n]
            if val == target:
                return True
            elif val < target:
                left = mid + 1
            else:
                right = mid - 1
        return False
java
// LC 74 - Search a 2D Matrix
// IDEA: Binary search treating matrix as flat 1D array; row = mid/n, col = mid%n
// time = O(log(M*N)), space = O(1)
public boolean searchMatrix(int[][] matrix, int target) {
    int m = matrix.length, n = matrix[0].length, l = 0, r = m * n - 1;
    while (l <= r) {
        int mid = (l + r) / 2, val = matrix[mid / n][mid % n];
        if (val == target) return true;
        else if (val < target) l = mid + 1;
        else r = mid - 1;
    }
    return false;
}

9) Search a 2D Matrix II — LC 240 — staircase elimination Priority 4 of 5 — High value — a gap here costs you rounds

Start from top-right corner; eliminate a row or column each step.

python
# LC 240 - Search a 2D Matrix II
# V0
# IDEA : Start from top-right, eliminate row/col each iteration
# Time: O(m+n), Space: O(1)
class Solution:
    def searchMatrix(self, matrix, target):
        if not matrix or not matrix[0]:
            return False
        row, col = 0, len(matrix[0]) - 1
        while row < len(matrix) and col >= 0:
            if matrix[row][col] == target:
                return True
            elif matrix[row][col] < target:
                row += 1      # eliminate current row
            else:
                col -= 1      # eliminate current column
        return False
java
// LC 240 - Search a 2D Matrix II
// IDEA: Start top-right; if val > target shrink col, if val < target grow row
// time = O(M+N), space = O(1)
public boolean searchMatrix(int[][] matrix, int target) {
    int row = 0, col = matrix[0].length - 1;
    while (row < matrix.length && col >= 0) {
        if (matrix[row][col] == target) return true;
        else if (matrix[row][col] < target) row++;
        else col--;
    }
    return false;
}

10) Kth Smallest Element in a Sorted Matrix — LC 378 — binary search on the answer

Rows and columns are sorted, but the matrix is not globally sorted — so LC 74’s “flatten to a 1-D sorted array” trick does not apply. Instead binary search the answer value, and count how many cells are <= mid with an O(n) staircase walk.

Key Idea (⭐⭐⭐⭐⭐ — the “binary search the answer” matrix pattern)

  1. Search space is the value range [matrix[0][0], matrix[n-1][n-1]], not indices.
  2. countLessOrEqual(target) walks from the bottom-left corner: if mat[r][c] <= target, the whole column above (r,c) also qualifies → cnt += r + 1, move right; else move up. O(n) per count.
  3. Shrink toward the smallest value v with count(v) >= k. That v is guaranteed to be an actual matrix element (the count only reaches k at a real value), so no membership check is needed.
LC 74 LC 240 LC 378
Matrix property fully sorted row-major row + col sorted row + col sorted
Search space index 0..m*n-1 cells value range
Move rule mid → (mid/n, mid%n) top-right staircase bottom-left staircase (counting)
Time O(log(m*n)) O(m+n) O(n·log(maxV-minV))
java
// LC 378 - Kth Smallest Element in a Sorted Matrix
// IDEA: binary search on VALUE range + O(n) staircase count of cells <= mid
// time = O(N * log(maxVal - minVal)), space = O(1)
public int kthSmallest(int[][] matrix, int k) {
    int n = matrix.length;
    int lo = matrix[0][0], hi = matrix[n - 1][n - 1];
    while (lo < hi) {
        int mid = lo + (hi - lo) / 2;           // avoid overflow
        if (countLessOrEqual(matrix, mid) >= k) hi = mid;   // enough → answer is <= mid
        else lo = mid + 1;                                  // too few → answer is > mid
    }
    return lo;   // lo == hi == smallest value whose count reaches k
}

// count cells <= target, walking from BOTTOM-LEFT
private int countLessOrEqual(int[][] mat, int target) {
    int n = mat.length, cnt = 0;
    int r = n - 1, c = 0;
    while (r >= 0 && c < n) {
        if (mat[r][c] <= target) { cnt += (r + 1); c++; }  // whole column up to r qualifies
        else r--;                                          // too big → move up
    }
    return cnt;
}
python
# LC 378 - Kth Smallest Element in a Sorted Matrix
# IDEA : BINARY SEARCH on value range + staircase counting from bottom-left
# Time: O(n * log(maxVal - minVal)), Space: O(1)
class Solution:
    def kthSmallest(self, matrix, k):
        n = len(matrix)

        def count_le(target):
            cnt, r, c = 0, n - 1, 0
            while r >= 0 and c < n:
                if matrix[r][c] <= target:
                    cnt += r + 1      # all cells above (r,c) in this column qualify
                    c += 1
                else:
                    r -= 1
            return cnt

        lo, hi = matrix[0][0], matrix[n-1][n-1]
        while lo < hi:
            mid = lo + (hi - lo) // 2
            if count_le(mid) >= k:
                hi = mid
            else:
                lo = mid + 1
        return lo

Dry-run — matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8:

text
lo=1, hi=15
mid=8   count<=8  = 2   (<8)  → lo=9
mid=12  count<=12 = 6   (<8)  → lo=13
mid=14  count<=14 = 8   (>=8) → hi=14
mid=13  count<=13 = 8   (>=8) → hi=13
lo == hi == 13  ✓

Variation — Find the Kth Smallest Sum of a Matrix With Sorted Rows (LC 1439)

Same “binary search the answer” skeleton, but the candidate values are row-combination sums; the counting step becomes a bounded DFS/heap over rows instead of a staircase walk. Simpler accepted alternative: fold rows one at a time, keeping only the k smallest sums after each merge.


DFS / BFS on a Grid

11) Number of Islands — LC 200 Priority 5 of 5 — Must know — expect it in almost every loop

Count connected components of '1’s by DFS-sinking each island.

python
# LC 200 - Number of Islands
# V0
# IDEA : DFS — sink each visited land cell to '0'
# Time: O(m*n), Space: O(m*n) recursion stack
class Solution(object):
    def numIslands(self, grid):
        def dfs(grid, x, y):
            if grid[y][x] == "0":
                return
            grid[y][x] = "0"
            for dx, dy in [(0,1),(0,-1),(1,0),(-1,0)]:
                nx, ny = x + dx, y + dy
                if 0 <= nx < w and 0 <= ny < l and grid[ny][nx] == "1":
                    dfs(grid, nx, ny)
        if not grid:
            return 0
        l, w = len(grid), len(grid[0])
        count = 0
        for i in range(l):
            for j in range(w):
                if grid[i][j] == "1":
                    count += 1
                    dfs(grid, j, i)
        return count
java
// LC 200 - Number of Islands
// IDEA: DFS from each unvisited '1'; sink visited cells to '0'
// time = O(M*N), space = O(M*N) recursion stack
public int numIslands(char[][] grid) {
    int count = 0;
    for (int i = 0; i < grid.length; i++)
        for (int j = 0; j < grid[0].length; j++)
            if (grid[i][j] == '1') { dfs(grid, i, j); count++; }
    return count;
}
private void dfs(char[][] grid, int i, int j) {
    if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] != '1') return;
    grid[i][j] = '0';
    dfs(grid, i+1, j); dfs(grid, i-1, j); dfs(grid, i, j+1); dfs(grid, i, j-1);
}

12) Longest Increasing Path in a Matrix — LC 329 — memoized DFS on a DAG

Key Idea: “strictly increasing” makes the grid a DAG — you can never revisit a cell on a path, so no visited set / backtracking is needed. Just memoize: memo[i][j] = longest increasing path starting at (i,j).

Why this is not ordinary flood fill: every edge points from a smaller to a larger value, so the graph is acyclic. Each cell’s answer depends only on strictly-larger neighbours, making it safe to cache (each cell computed once → O(m·n)).

java
// LC 329 - Longest Increasing Path in a Matrix
// IDEA: DFS + memo on a DAG (edges go small -> large, so no cycle, no visited set)
// time = O(M*N), space = O(M*N)
private static final int[][] DIRS = {{0,1},{0,-1},{1,0},{-1,0}};

public int longestIncreasingPath(int[][] matrix) {
    if (matrix == null || matrix.length == 0 || matrix[0].length == 0) return 0;
    int m = matrix.length, n = matrix[0].length;
    int[][] memo = new int[m][n];        // 0 = not computed yet
    int best = 0;
    for (int i = 0; i < m; i++)
        for (int j = 0; j < n; j++)
            best = Math.max(best, dfs(matrix, i, j, memo));
    return best;
}

private int dfs(int[][] mat, int i, int j, int[][] memo) {
    if (memo[i][j] != 0) return memo[i][j];   // cached
    int best = 1;                              // the cell itself
    for (int[] d : DIRS) {
        int r = i + d[0], c = j + d[1];
        if (r < 0 || r >= mat.length || c < 0 || c >= mat[0].length) continue;
        if (mat[r][c] <= mat[i][j]) continue;  // must strictly increase
        best = Math.max(best, 1 + dfs(mat, r, c, memo));
    }
    memo[i][j] = best;
    return best;
}
python
# LC 329 - Longest Increasing Path in a Matrix
# IDEA : DFS + MEMOIZATION on a DAG (strictly increasing => acyclic => no visited set)
# Time: O(m*n), Space: O(m*n)
from functools import lru_cache

class Solution:
    def longestIncreasingPath(self, matrix):
        if not matrix or not matrix[0]:
            return 0
        m, n = len(matrix), len(matrix[0])

        @lru_cache(maxsize=None)
        def dfs(i, j):
            best = 1
            for di, dj in ((0,1), (0,-1), (1,0), (-1,0)):
                r, c = i + di, j + dj
                if 0 <= r < m and 0 <= c < n and matrix[r][c] > matrix[i][j]:
                    best = max(best, 1 + dfs(r, c))
            return best

        return max(dfs(i, j) for i in range(m) for j in range(n))

Common mistakes

  • Adding a visited set + backtracking → correct but O(4^(m·n)); the memo is what makes it linear.
  • Using >= instead of > → creates cycles among equal values and infinite recursion.
  • Initializing memo with 0 is safe only because a real answer is always >= 1.

2D Dynamic Programming

13) Minimum Path Sum — LC 64

DP where each cell accumulates the minimum cost to reach it.

python
# LC 64 - Minimum Path Sum
# V0
# IDEA : DP — dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
# Time: O(m*n), Space: O(1) (modify grid in-place)
class Solution:
    def minPathSum(self, grid):
        if not grid:
            return 0
        m, n = len(grid), len(grid[0])
        # fill first column
        for i in range(1, m):
            grid[i][0] += grid[i-1][0]
        # fill first row
        for j in range(1, n):
            grid[0][j] += grid[0][j-1]
        # fill rest
        for i in range(1, m):
            for j in range(1, n):
                grid[i][j] += min(grid[i-1][j], grid[i][j-1])
        return grid[-1][-1]
java
// LC 64 - Minimum Path Sum
// IDEA: DP in-place; dp[i][j] += min(dp[i-1][j], dp[i][j-1])
// time = O(M*N), space = O(1)
public int minPathSum(int[][] grid) {
    int m = grid.length, n = grid[0].length;
    for (int i = 1; i < m; i++) grid[i][0] += grid[i-1][0];
    for (int j = 1; j < n; j++) grid[0][j] += grid[0][j-1];
    for (int i = 1; i < m; i++)
        for (int j = 1; j < n; j++)
            grid[i][j] += Math.min(grid[i-1][j], grid[i][j-1]);
    return grid[m-1][n-1];
}

14) Maximal Square — LC 221 Priority 3 of 5 — Worth knowing — usually a variant of a must-know pattern

dp[i][j] = side length of largest square with bottom-right at (i,j).

python
# LC 221 - Maximal Square
# V0
# IDEA : DP — dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
# Time: O(m*n), Space: O(m*n)
class Solution:
    def maximalSquare(self, matrix):
        if not matrix:
            return 0
        m, n = len(matrix), len(matrix[0])
        dp = [[0] * n for _ in range(m)]
        ans = 0
        for i in range(m):
            for j in range(n):
                dp[i][j] = int(matrix[i][j])
                if i and j and dp[i][j]:
                    dp[i][j] = min(dp[i-1][j-1], dp[i][j-1], dp[i-1][j]) + 1
                ans = max(ans, dp[i][j])
        return ans * ans
java
// LC 221 - Maximal Square
// IDEA: dp[i][j] = min(left, top, diag) + 1 when cell is '1'; ans = max dp^2
// time = O(M*N), space = O(M*N)
public int maximalSquare(char[][] matrix) {
    int m = matrix.length, n = matrix[0].length, ans = 0;
    int[][] dp = new int[m + 1][n + 1];
    for (int i = 1; i <= m; i++)
        for (int j = 1; j <= n; j++)
            if (matrix[i-1][j-1] == '1') {
                dp[i][j] = Math.min(dp[i-1][j-1], Math.min(dp[i-1][j], dp[i][j-1])) + 1;
                ans = Math.max(ans, dp[i][j]);
            }
    return ans * ans;
}

Variation — Count Square Submatrices with All Ones (LC 1277)

Same min(left, top, diag) + 1 recurrence — the twist is sum every dp value instead of taking the max: a cell whose dp[i][j] == k is the bottom-right corner of exactly k all-ones squares (sizes 1…k).

java
// LC 1277 - Count Square Submatrices with All Ones
// IDEA: Maximal Square DP, but accumulate dp[i][j] instead of max; reuse grid as dp table
// time = O(M*N), space = O(1)
public int countSquares(int[][] matrix) {
    int m = matrix.length, n = matrix[0].length, total = 0;
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (matrix[i][j] == 1 && i > 0 && j > 0) {
                matrix[i][j] = 1 + Math.min(matrix[i-1][j-1],
                                   Math.min(matrix[i-1][j], matrix[i][j-1]));
            }
            total += matrix[i][j];   // dp[i][j] squares end at (i,j)
        }
    }
    return total;
}
python
# LC 1277 - Count Square Submatrices with All Ones
# IDEA : Maximal Square DP, but accumulate dp[i][j] instead of max
# Time: O(m*n), Space: O(1) (in-place)
class Solution:
    def countSquares(self, matrix):
        m, n = len(matrix), len(matrix[0])
        total = 0
        for i in range(m):
            for j in range(n):
                if matrix[i][j] == 1 and i > 0 and j > 0:
                    matrix[i][j] = 1 + min(matrix[i-1][j-1], matrix[i-1][j], matrix[i][j-1])
                total += matrix[i][j]
        return total

Related: LC 1504 Count Submatrices With All Ones counts rectangles (not just squares) — the square DP no longer applies; use per-column consecutive-ones heights + a monotonic stack (see section 2-15 below).


15) Maximal Rectangle — LC 85 — row-by-row histogram reduction

Key Idea (⭐⭐⭐⭐⭐): reduce a 2-D problem to a 1-D one. Scan rows top→bottom keeping heights[j] = number of consecutive 1s ending at the current row in column j. Each row is then a histogram → run Largest Rectangle in Histogram (LC 84) on it and take the max.

text
matrix                heights after each row
1 0 1 0 0             [1,0,1,0,0]  → max area 1
1 0 1 1 1             [2,0,2,1,1]  → max area 3
1 1 1 1 1             [3,1,3,2,2]  → max area 6   ← answer
1 0 0 1 0             [4,0,0,3,0]  → max area 4
java
// LC 85 - Maximal Rectangle
// IDEA: per-row histogram of consecutive 1s + LC 84 monotonic stack
// time = O(M*N), space = O(N)
public int maximalRectangle(char[][] matrix) {
    if (matrix == null || matrix.length == 0 || matrix[0].length == 0) return 0;
    int n = matrix[0].length, best = 0;
    int[] heights = new int[n];
    for (char[] row : matrix) {
        // build histogram for this row: reset to 0 on '0', else grow
        for (int j = 0; j < n; j++) heights[j] = (row[j] == '1') ? heights[j] + 1 : 0;
        best = Math.max(best, largestRectangleArea(heights));
    }
    return best;
}

// LC 84 - Largest Rectangle in Histogram (increasing monotonic stack of indices)
private int largestRectangleArea(int[] h) {
    int n = h.length, best = 0;
    Deque<Integer> st = new ArrayDeque<>();
    for (int i = 0; i <= n; i++) {
        int cur = (i == n) ? 0 : h[i];          // sentinel 0 flushes the stack
        while (!st.isEmpty() && h[st.peek()] >= cur) {
            int height = h[st.pop()];
            int left = st.isEmpty() ? -1 : st.peek();   // previous smaller index
            best = Math.max(best, height * (i - left - 1));
        }
        st.push(i);
    }
    return best;
}
python
# LC 85 - Maximal Rectangle
# IDEA : per-row histogram of consecutive 1s + LC 84 monotonic stack
# Time: O(m*n), Space: O(n)
class Solution:
    def maximalRectangle(self, matrix):
        if not matrix or not matrix[0]:
            return 0
        n = len(matrix[0])
        heights = [0] * n
        best = 0
        for row in matrix:
            for j in range(n):
                heights[j] = heights[j] + 1 if row[j] == '1' else 0
            best = max(best, self.largestRectangleArea(heights))
        return best

    def largestRectangleArea(self, h):
        st, best = [], 0
        for i in range(len(h) + 1):
            cur = 0 if i == len(h) else h[i]      # sentinel flush
            while st and h[st[-1]] >= cur:
                height = h[st.pop()]
                left = st[-1] if st else -1
                best = max(best, height * (i - left - 1))
            st.append(i)
        return best

Related problems using the same row-histogram reduction:

Problem LC # Twist
Maximal Rectangle 85 max area of an all-1 rectangle
Count Submatrices With All Ones 1504 count all-1 rectangles instead of maximizing (stack keeps a running per-column sum)
Maximal Square 221 squares only → simpler min(left, top, diag)+1 DP (see 14) Maximal Square)

Prefix Sums & Row-Pair Compression

16) Matrix Block Sum — LC 1314 — 2D prefix sum

Build 2D prefix sum matrix, then query O(1) for each cell’s block sum.

java
// LC 1314 - Matrix Block Sum
// V0
// IDEA: 2D Prefix Sum (Summed-Area Table)
// Time: O(m*n), Space: O(m*n)

/**
 * Key Insight:
 * - Without prefix sum: O(m*n*k²) — for each cell, scan k×k block
 * - With prefix sum: O(m*n) build + O(1) per query
 *
 * Formula:
 * - Build:  pref[i+1][j+1] = mat[i][j] + pref[i][j+1] + pref[i+1][j] - pref[i][j]
 * - Query:  sum = pref[r2+1][c2+1] - pref[r1][c2+1] - pref[r2+1][c1] + pref[r1][c1]
 *
 * The +1 offset allows pref[0][j] and pref[i][0] to be zero padding,
 * preventing IndexOutOfBounds when querying edges.
 */
public int[][] matrixBlockSum(int[][] mat, int k) {
    int m = mat.length;
    int n = mat[0].length;

    // 1. Build 2D prefix sum matrix (size m+1 x n+1)
    int[][] pref = new int[m + 1][n + 1];
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            pref[i + 1][j + 1] = mat[i][j]
                    + pref[i][j + 1]      // top
                    + pref[i + 1][j]      // left
                    - pref[i][j];         // top-left (subtracted twice)
        }
    }

    int[][] res = new int[m][n];

    // 2. Calculate sum for each block [i-k, j-k] to [i+k, j+k]
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            // Clamp boundaries to valid matrix indices
            int r1 = Math.max(0, i - k);
            int c1 = Math.max(0, j - k);
            int r2 = Math.min(m - 1, i + k);
            int c2 = Math.min(n - 1, j + k);

            // Query using prefix sum formula (adjust for 1-based pref)
            res[i][j] = pref[r2 + 1][c2 + 1]
                    - pref[r1][c2 + 1]    // subtract top region
                    - pref[r2 + 1][c1]    // subtract left region
                    + pref[r1][c1];       // add back top-left (double subtracted)
        }
    }

    return res;
}

Visual Explanation of 2D Prefix Sum Query:

text
For rectangle (r1,c1) to (r2,c2):

     0    c1        c2   n
   ┌──────┬─────────┬────┐
 0 │      │    A    │    │
   │      │         │    │
r1 ├──────┼─────────┼────┤
   │      │         │    │
   │  C   │ TARGET  │    │
   │      │         │    │
r2 ├──────┼─────────┼────┤
   │      │         │    │
 m └──────┴─────────┴────┘

TARGET = pref[r2+1][c2+1] - A - C + TopLeft
       = pref[r2+1][c2+1] - pref[r1][c2+1] - pref[r2+1][c1] + pref[r1][c1]

Similar Problems:

  • LC 304: Range Sum Query 2D - Immutable (same 2D prefix sum)
  • LC 308: Range Sum Query 2D - Mutable (needs segment tree / BIT)
  • LC 1292: Maximum Side Length of Square (2D prefix sum + binary search)

17) Number of Submatrices That Sum to Target — LC 1074 — row-pair compression

Key Idea (⭐⭐⭐⭐⭐): fix a pair of columns (or rows), collapse the strip between them into a 1-D array, then apply the 1-D “subarray sum equals K” hashmap trick. This turns any “count/optimize over all submatrices” problem into O(n²) strips × a 1-D scan.

Recipe

  1. Prefix-sum each row so a strip sum [c1..c2] of row i is O(1).
  2. For every column pair (c1 <= c2): walk rows accumulating sum, and count previously seen prefixes equal to sum - target via a HashMap seeded with {0: 1}.
  3. Total: O(m·n²) time, O(m) extra space (choose the smaller dimension as the “pair” dimension).
java
// LC 1074 - Number of Submatrices That Sum to Target
// IDEA: row prefix sums -> fix column pair (c1,c2) -> 1-D "subarray sum == target" hashmap
// time = O(M*N*N), space = O(M)
// NOTE: mutates the input matrix into row prefix sums; copy first if that matters
public int numSubmatrixSumTarget(int[][] matrix, int target) {
    int m = matrix.length, n = matrix[0].length;

    // 1. prefix sum along each row
    for (int i = 0; i < m; i++)
        for (int j = 1; j < n; j++)
            matrix[i][j] += matrix[i][j - 1];

    int res = 0;
    Map<Integer, Integer> cnt = new HashMap<>();
    // 2. every column pair defines a vertical strip
    for (int c1 = 0; c1 < n; c1++) {
        for (int c2 = c1; c2 < n; c2++) {
            cnt.clear();
            cnt.put(0, 1);          // empty prefix
            int sum = 0;
            // 3. 1-D subarray-sum-equals-target scan down the rows
            for (int i = 0; i < m; i++) {
                sum += matrix[i][c2] - (c1 > 0 ? matrix[i][c1 - 1] : 0);
                res += cnt.getOrDefault(sum - target, 0);
                cnt.merge(sum, 1, Integer::sum);
            }
        }
    }
    return res;
}
python
# LC 1074 - Number of Submatrices That Sum to Target
# IDEA : row prefix sums -> fix column pair -> 1-D subarray-sum-equals-target hashmap
# Time: O(m*n^2), Space: O(m)
from collections import defaultdict

class Solution:
    def numSubmatrixSumTarget(self, matrix, target):
        m, n = len(matrix), len(matrix[0])
        # 1. prefix sum along each row
        for i in range(m):
            for j in range(1, n):
                matrix[i][j] += matrix[i][j-1]

        res = 0
        for c1 in range(n):                 # 2. fix left column
            for c2 in range(c1, n):         #    fix right column
                cnt = defaultdict(int)
                cnt[0] = 1
                cur = 0
                for i in range(m):          # 3. scan rows as a 1-D array
                    cur += matrix[i][c2] - (matrix[i][c1-1] if c1 > 0 else 0)
                    res += cnt[cur - target]
                    cnt[cur] += 1
        return res

Variation — Max Sum of Rectangle No Larger Than K (LC 363)

Same row-pair compression, but the inner 1-D step changes from “hashmap equality lookup” to “find the smallest prefix >= cur - k” in a sorted structure (Java TreeSet.ceiling, Python sortedcontainers / bisect on a maintained sorted list) → O(m·n²·log m).

Compression cheat-sheet

Goal on all submatrices Inner 1-D routine LC
Count sums == target HashMap of prefix counts 1074
Max sum <= K sorted set + ceiling 363
Max sum (unbounded) Kadane’s algorithm
Any-rectangle range sum 2-D prefix sum (see 16) Matrix Block Sum) 304, 1314