Set Matrix Zeroes: Optimal O(1) Space In-Place Algorithm, Boundary Markers & Cache Locality

The Set Matrix Zeroes problem (LeetCode 73) is a foundational interview benchmark in algorithmic optimization and spatial memory efficiency. The problem statement appears straightforward: given an m × n integer matrix, if any element is 0, set its entire row and column to 0. However, the true engineering challenge lies in the spatial constraint: while an O(m × n) or O(m + n) solution is trivial, an optimal solution must execute strictly in-place with O(1) auxiliary memory. In this architectural guide, we dissect the state machine required to convert matrix boundary cells into reusable marker arrays, examine the collision trap at coordinate (0, 0), analyze row-major CPU cache locality, and provide verifiable implementations in modern C++20 and TypeScript.

1. Complexity Hierarchy: From Naive Duplication to In-Place Reuse

When approaching matrix mutation problems, inexperienced engineers frequently stumble into two common pitfalls: either creating duplicate state that violates memory bounds, or prematurely overwriting cells that cascade false zeros throughout subsequent iterations:

  • The Cascading Corruption Trap (In-Place Naive): If you iterate through a matrix and immediately overwrite an entire row and column with zeros upon encountering a zero, newly written zeros will trigger subsequent row and column wipeouts. By the time the nested loop finishes, the entire matrix is filled with zeros.
  • The O(m × n) Auxiliary Clone: Creating a deep copy of the original m × n matrix allows you to read original states while writing zeros to the clone. While mathematically correct, this allocates substantial heap memory (e.g., allocating a 10,000 × 10,000 integer matrix requires roughly 400 MB of RAM), which fails enterprise memory limits and technical evaluations.
  • The O(m + n) Vector Flags: Allocating two boolean arrays—vector<bool> zeroRows(m) and vector<bool> zeroCols(n)—records which rows and columns require zeroing in an initial pass, followed by a secondary pass to fill values. While practical for small matrices, it still demands auxiliary heap allocations.
  • The Optimal O(1) Boundary Marker Strategy: Rather than allocating external vectors, we project the flag markers directly into the matrix itself. We utilize the first row (mat[0][j]) and the first column (mat[i][0]) as our internal boolean marker arrays, completely eliminating auxiliary heap allocations.
In-Place Marker Strategy: Utilizing Row 0 and Col 0 as State Registers mat[0][0] Collision Cell mat[0][1] Col 1 Flag mat[0][2] Col 2 Flag mat[1][0] Row 1 Flag mat[2][0] Row 2 Flag Inner Matrix [1..m-1][1..n-1] Scanned during Pass 1 & Updated in Pass 2 Algorithmic Execution Order Step 1: Inspect Perimeter Record if row 0 or col 0 originally contains a zero into isolated scalar booleans: firstRowHasZero, firstColHasZero. Step 2: Scan Inner Core For i ∈ [1, m-1], j ∈ [1, n-1]: If mat[i][j] == 0, mark mat[i][0] = 0 (row flag) and mat[0][j] = 0 (col flag). Step 3: Resolve Dependencies Zero inner cells first, then apply firstRow/Col flags last.

2. The Coordinate (0, 0) Collision Trap

The primary architectural difficulty with in-place matrix marking is the geometric intersection at cell (0, 0). Cell mat[0][0] belongs to both the first row and the first column. If a zero is detected in column 0 and marked in mat[0][0], a naive implementation might interpret that mark as an indication that row 0 should also be zeroed out, corrupting the entire first row even if it contained no original zeros.

To decouple this intersection, we isolate the states:

  • We dedicate mat[0][0] to represent the status of row 0 (or column 0).
  • We allocate a single primitive boolean scalar—firstColHasZero—to store the independent status of the other boundary. Because a scalar primitive consumes a single machine register (or a byte on the stack), our auxiliary memory complexity remains strictly O(1).
  • We process the matrix in reverse or bottom-up order during the application phase, or update the inner submatrix (1..m-1, 1..n-1) before finally touching the first row and column.

3. Hardware Cache Locality & Memory Layout

In modern computer architectures, memory performance is dominated by the CPU cache hierarchy (L1, L2, L3 caches). In languages like C++, 2D vectors or matrices are typically stored in contiguous row-major order. When iterating sequentially across columns within a fixed row (mat[i][0] → mat[i][1] → mat[i][2]), each memory access fetches an entire 64-byte cache line from DRAM, delivering near 100% L1 cache hit rates.

Conversely, iterating down a column across consecutive rows (mat[0][j] → mat[1][j] → mat[2][j]) introduces a memory stride of n × sizeof(int). If the matrix width n is large, every access misses the L1 cache, triggering costly DRAM line fetches. Therefore, high-performance implementations structure the nested loops to maintain row-major traversal across the inner core, minimizing CPU cache stall cycles.

4. Production C++20 Implementation

Below is the complete, high-performance C++20 implementation of the in-place Set Matrix Zeroes algorithm, handling all boundary edge cases cleanly:

#include <vector>
#include <cstddef>

class Solution {
public:
    void setZeroes(std::vector<std::vector<int>>& matrix) {
        if (matrix.empty() || matrix[0].empty()) return;

        const size_t m = matrix.size();
        const size_t n = matrix[0].size();

        bool firstRowHasZero = false;
        bool firstColHasZero = false;

        // 1. Check if first row originally has any zero
        for (size_t j = 0; j < n; ++j) {
            if (matrix[0][j] == 0) {
                firstRowHasZero = true;
                break;
            }
        }

        // 2. Check if first column originally has any zero
        for (size_t i = 0; i < m; ++i) {
            if (matrix[i][0] == 0) {
                firstColHasZero = true;
                break;
            }
        }

        // 3. Scan the inner submatrix and project flags into row 0 and col 0
        for (size_t i = 1; i < m; ++i) {
            for (size_t j = 1; j < n; ++j) {
                if (matrix[i][j] == 0) {
                    matrix[i][0] = 0;
                    matrix[0][j] = 0;
                }
            }
        }

        // 4. Update the inner submatrix based on boundary markers (row-major order)
        for (size_t i = 1; i < m; ++i) {
            for (size_t j = 1; j < n; ++j) {
                if (matrix[i][0] == 0 || matrix[0][j] == 0) {
                    matrix[i][j] = 0;
                }
            }
        }

        // 5. Zero out first row if originally needed
        if (firstRowHasZero) {
            for (size_t j = 0; j < n; ++j) {
                matrix[0][j] = 0;
            }
        }

        // 6. Zero out first column if originally needed
        if (firstColHasZero) {
            for (size_t i = 0; i < m; ++i) {
                matrix[i][0] = 0;
            }
        }
    }
};

5. Production TypeScript Implementation

In full-stack JavaScript and TypeScript applications operating on grid structures (e.g., spreadsheet calculations, canvas bitmap matrices), the identical O(1) auxiliary space logic applies:

/**
 * In-place Set Matrix Zeroes in TypeScript
 * Time Complexity: O(m * n)
 * Auxiliary Space Complexity: O(1)
 */
export function setZeroes(matrix: number[][]): void {
  const m = matrix.length;
  if (m === 0) return;
  const n = matrix[0].length;
  if (n === 0) return;

  let firstRowHasZero = false;
  let firstColHasZero = false;

  // Step 1: Detect zero presence on boundaries
  for (let j = 0; j < n; j++) {
    if (matrix[0][j] === 0) {
      firstRowHasZero = true;
      break;
    }
  }

  for (let i = 0; i < m; i++) {
    if (matrix[i][0] === 0) {
      firstColHasZero = true;
      break;
    }
  }

  // Step 2: Use row 0 and col 0 as registers for inner cells
  for (let i = 1; i < m; i++) {
    for (let j = 1; j < n; j++) {
      if (matrix[i][j] === 0) {
        matrix[i][0] = 0;
        matrix[0][j] = 0;
      }
    }
  }

  // Step 3: Populate inner cells from registers
  for (let i = 1; i < m; i++) {
    for (let j = 1; j < n; j++) {
      if (matrix[i][0] === 0 || matrix[0][j] === 0) {
        matrix[i][j] = 0;
      }
    }
  }

  // Step 4: Finalize row 0
  if (firstRowHasZero) {
    for (let j = 0; j < n; j++) {
      matrix[0][j] = 0;
    }
  }

  // Step 5: Finalize col 0
  if (firstColHasZero) {
    for (let i = 0; i < m; i++) {
      matrix[i][0] = 0;
    }
  }
}

6. Invariant Proof & Architectural Takeaway

The mathematical correctness of the algorithm rests on strict temporal ordering: by deferring the mutation of the boundary registers (row 0 and column 0) until after the entire inner core [1..m-1][1..n-1] has consulted those registers, we guarantee that no information is prematurely destroyed. Spatial memory optimization in computer science frequently relies on this exact principle: converting existing data buffers into transient state machines to eliminate redundant heap allocations.