title: Set Matrix Zeroes
slug: set-matrix-zeroes
difficulty: medium
leetcode_id: 73
leetcode_url: https://leetcode.com/problems/set-matrix-zeroes/
categories:
  - arrays
  - hash-tables
patterns:
  - matrix-traversal

function_signature: "def set_zeroes(matrix: list[list[int]]) -> None:"

test_cases:
  visible:
    - input: { matrix: [[1, 1, 1], [1, 0, 1], [1, 1, 1]] }
      expected: [[1, 0, 1], [0, 0, 0], [1, 0, 1]]
    - input: { matrix: [[0, 1, 2, 0], [3, 4, 5, 2], [1, 3, 1, 5]] }
      expected: [[0, 0, 0, 0], [0, 4, 5, 0], [0, 3, 1, 0]]
  hidden:
    - input: { matrix: [[1, 2, 3], [4, 5, 6]] }
      expected: [[1, 2, 3], [4, 5, 6]]
    - input: { matrix: [[0]] }
      expected: [[0]]
    - input: { matrix: [[1, 0, 3]] }
      expected: [[0, 0, 0]]

description: |
  Given an `m x n` integer matrix `matrix`, if an element is `0`, set its entire row and column to `0`'s.

  You must do it **in place**.

constraints: |
  - `m == matrix.length`
  - `n == matrix[0].length`
  - `1 <= m, n <= 200`
  - `-2^31 <= matrix[i][j] <= 2^31 - 1`

examples:
  - input: "matrix = [[1,1,1],[1,0,1],[1,1,1]]"
    output: "[[1,0,1],[0,0,0],[1,0,1]]"
    explanation: "The element at position (1,1) is 0, so its entire row (row 1) and column (column 1) are set to 0."
  - input: "matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]"
    output: "[[0,0,0,0],[0,4,5,0],[0,3,1,0]]"
    explanation: "Zeroes at positions (0,0) and (0,3) cause row 0 and columns 0 and 3 to be zeroed. The middle elements remain unchanged where rows and columns don't intersect with zeroes."

explanation:
  intuition: |
    Imagine you're playing a game where every zero in a grid is a "bomb" that explodes horizontally and vertically, turning everything in its row and column to zero.

    The challenge is that you can't simply iterate through and set values to zero as you find bombs — if you do, you'll create new zeroes that weren't originally there, causing a chain reaction that zeros out the entire matrix.

    Think of it like this: you need to first **survey the battlefield** to find all the original bombs, then **detonate them all at once**. The question is: how do we remember where all the bombs are without using extra space proportional to the matrix size?

    The key insight is that we can use the **matrix itself as a notepad**. Specifically, we can use the first row and first column as markers to record which rows and columns need to be zeroed. Since these cells will eventually be overwritten anyway (if they need to be zeroed), we're not losing any information — we just need to handle the first row and column specially.

  approach: |
    We solve this using the **In-Place Marker Approach** with O(1) extra space:

    **Step 1: Check if first row and first column need zeroing**

    - `first_row_has_zero`: Scan the first row for any zero
    - `first_col_has_zero`: Scan the first column for any zero
    - We need these flags because we'll overwrite the first row/column with markers

    &nbsp;

    **Step 2: Use first row and column as markers**

    - Iterate through the matrix (excluding first row/column)
    - If `matrix[i][j] == 0`, set `matrix[i][0] = 0` and `matrix[0][j] = 0`
    - This marks row `i` and column `j` for zeroing

    &nbsp;

    **Step 3: Zero out cells based on markers**

    - Iterate through the matrix (excluding first row/column)
    - If `matrix[i][0] == 0` or `matrix[0][j] == 0`, set `matrix[i][j] = 0`
    - This propagates the zeroes based on our markers

    &nbsp;

    **Step 4: Handle first row and column**

    - If `first_row_has_zero` was true, zero out the entire first row
    - If `first_col_has_zero` was true, zero out the entire first column
    - This must be done **last** to avoid corrupting our markers

    &nbsp;

    This approach cleverly reuses space we were going to modify anyway, achieving constant extra space.

  common_pitfalls:
    - title: Modifying While Iterating
      description: |
        The most common mistake is setting cells to zero as you find zeroes:

        ```python
        for i in range(m):
            for j in range(n):
                if matrix[i][j] == 0:
                    # Set row and column to zero immediately
                    for k in range(n): matrix[i][k] = 0
                    for k in range(m): matrix[k][j] = 0
        ```

        This creates **new zeroes** that weren't originally there. When you later encounter these new zeroes, you'll zero out even more rows and columns, potentially turning the entire matrix to zeroes.

        The fix is to first **record** all original zero positions, then **apply** the changes in a second pass.
      wrong_approach: "Setting cells to zero immediately when a zero is found"
      correct_approach: "First pass to mark, second pass to zero"

    - title: Using O(m*n) Space
      description: |
        A straightforward but wasteful approach is to create a copy of the matrix:

        ```python
        copy = [[matrix[i][j] for j in range(n)] for i in range(m)]
        ```

        This uses O(m*n) extra space. The problem specifically asks for an in-place solution, and the follow-up challenges you to use O(1) space.

        Even using two sets to store which rows and columns need zeroing uses O(m + n) space — better, but not optimal.
      wrong_approach: "Copying the entire matrix or storing all zero positions"
      correct_approach: "Use the first row and column as markers"

    - title: Corrupting Markers Before Using Them
      description: |
        When using the first row/column as markers, a subtle bug is handling them in the wrong order:

        ```python
        # WRONG: Zeroing first row/column early corrupts markers
        if first_row_has_zero:
            for j in range(n): matrix[0][j] = 0  # Destroys column markers!
        ```

        If you zero the first row before using its values as markers, you lose the information about which columns need zeroing.

        The fix is to **always handle the first row and column last**, after all other cells have been processed.
      wrong_approach: "Zeroing first row/column before processing other cells"
      correct_approach: "Process interior cells first, then handle first row/column last"

  key_takeaways:
    - "**In-place markers**: When you need to mark items for later processing, consider reusing space that will be overwritten anyway"
    - "**Two-pass pattern**: Many matrix problems benefit from separating 'marking' and 'applying' into distinct passes to avoid corrupting data"
    - "**Order matters**: When using part of the input as auxiliary storage, process it last to avoid destroying information you still need"
    - "**Space optimisation progression**: This problem illustrates a common pattern — O(mn) -> O(m+n) -> O(1) — each step requiring more clever use of existing space"

  time_complexity: "O(m * n). We traverse the entire matrix a constant number of times (marking pass + zeroing pass)."
  space_complexity: "O(1). We only use two boolean variables (`first_row_has_zero` and `first_col_has_zero`), regardless of matrix size."

solutions:
  - approach_name: In-Place Markers
    is_optimal: true
    code: |
      def set_zeroes(matrix: list[list[int]]) -> None:
          """
          Modify matrix in-place to zero out rows and columns containing zeroes.
          """
          m, n = len(matrix), len(matrix[0])

          # Step 1: Check if first row/column originally have zeroes
          first_row_has_zero = any(matrix[0][j] == 0 for j in range(n))
          first_col_has_zero = any(matrix[i][0] == 0 for i in range(m))

          # Step 2: Use first row/column as markers for the rest of the matrix
          for i in range(1, m):
              for j in range(1, n):
                  if matrix[i][j] == 0:
                      matrix[i][0] = 0  # Mark this row
                      matrix[0][j] = 0  # Mark this column

          # Step 3: Zero out cells based on markers (excluding first row/column)
          for i in range(1, m):
              for j in range(1, n):
                  if matrix[i][0] == 0 or matrix[0][j] == 0:
                      matrix[i][j] = 0

          # Step 4: Handle first row and column last
          if first_row_has_zero:
              for j in range(n):
                  matrix[0][j] = 0

          if first_col_has_zero:
              for i in range(m):
                  matrix[i][0] = 0
    explanation: |
      **Time Complexity:** O(m * n) — We make two passes through the matrix.

      **Space Complexity:** O(1) — Only two boolean flags used.

      This solution cleverly uses the first row and column as storage for markers, achieving constant space. The key is processing the first row/column last so we don't corrupt our markers.

  - approach_name: Hash Sets
    is_optimal: false
    code: |
      def set_zeroes(matrix: list[list[int]]) -> None:
          """
          Uses O(m + n) space to track which rows and columns to zero.
          """
          m, n = len(matrix), len(matrix[0])

          # Track which rows and columns contain zeroes
          zero_rows = set()
          zero_cols = set()

          # First pass: find all zeroes
          for i in range(m):
              for j in range(n):
                  if matrix[i][j] == 0:
                      zero_rows.add(i)
                      zero_cols.add(j)

          # Second pass: zero out marked rows and columns
          for i in range(m):
              for j in range(n):
                  if i in zero_rows or j in zero_cols:
                      matrix[i][j] = 0
    explanation: |
      **Time Complexity:** O(m * n) — Two passes through the matrix.

      **Space Complexity:** O(m + n) — Sets can contain at most m rows and n columns.

      This approach is more intuitive but uses extra space. It's a good stepping stone to understanding the optimal solution — the key insight is that we can encode the same information in the matrix itself.
