title: 01 Matrix
slug: 01-matrix
difficulty: medium
leetcode_id: 542
leetcode_url: https://leetcode.com/problems/01-matrix/
categories:
  - arrays
  - graphs
patterns:
  - bfs
  - matrix-traversal
  - dynamic-programming

function_signature: "def update_matrix(mat: list[list[int]]) -> list[list[int]]:"

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

description: |
  Given an `m x n` binary matrix `mat`, return *the distance of the nearest* `0` *for each cell*.

  The distance between two cells sharing a common edge is `1`.

constraints: |
  - `m == mat.length`
  - `n == mat[i].length`
  - `1 <= m, n <= 10^4`
  - `1 <= m * n <= 10^4`
  - `mat[i][j]` is either `0` or `1`
  - There is at least one `0` in `mat`

examples:
  - input: "mat = [[0,0,0],[0,1,0],[0,0,0]]"
    output: "[[0,0,0],[0,1,0],[0,0,0]]"
    explanation: "The center cell has value 1, and its nearest 0 is any of its four adjacent cells, so its distance is 1. All other cells are already 0."
  - input: "mat = [[0,0,0],[0,1,0],[1,1,1]]"
    output: "[[0,0,0],[0,1,0],[1,2,1]]"
    explanation: "The bottom-middle cell (1,1) has distance 2 because its nearest 0 is two steps away (e.g., up then up, or up then left/right)."

explanation:
  intuition: |
    Imagine you're standing at each `1` cell and need to find the shortest path to any `0` cell. This sounds like a shortest path problem, and in an unweighted grid, **BFS** finds shortest paths.

    The key insight is to **reverse the perspective**: instead of starting from each `1` and searching for `0`s (which would be inefficient), start from **all `0` cells simultaneously** and expand outward. Think of it like dropping stones into a pond at every `0` position — the ripples spread outward, and each `1` cell records when the first ripple reaches it.

    This is called **multi-source BFS**. By starting from all sources at once, each cell is visited exactly once, and the first time we reach any cell is guaranteed to be via the shortest path from some `0`.

    Alternatively, you can solve this with **dynamic programming** using two passes: one top-left to bottom-right, and one bottom-right to top-left. Each pass propagates minimum distances from the directions already processed.

  approach: |
    We solve this using **Multi-source BFS**:

    **Step 1: Initialise the result matrix and queue**

    - Create a `dist` matrix of the same size as `mat`
    - Set `dist[i][j] = 0` for all cells where `mat[i][j] == 0`, and add these to the BFS queue
    - Set `dist[i][j] = infinity` for all cells where `mat[i][j] == 1`

    &nbsp;

    **Step 2: Perform BFS from all zeros simultaneously**

    - While the queue is not empty, dequeue a cell `(r, c)`
    - For each of its four neighbours `(nr, nc)`:
      - If `dist[r][c] + 1 < dist[nr][nc]`, we found a shorter path
      - Update `dist[nr][nc] = dist[r][c] + 1`
      - Add `(nr, nc)` to the queue

    &nbsp;

    **Step 3: Return the result**

    - Return the `dist` matrix after BFS completes

    &nbsp;

    The BFS guarantees that when we first reach a cell, it's via the shortest path from some zero. Since all zeros start at distance 0 and expand level by level, distance 1 cells are processed before distance 2 cells, and so on.

  common_pitfalls:
    - title: Running BFS from Each Cell
      description: |
        A naive approach is to run BFS from each `1` cell to find the nearest `0`. This results in **O((m*n)^2)** time complexity in the worst case.

        With the constraint `m * n <= 10^4`, this means up to 100 million operations — likely too slow.

        Multi-source BFS visits each cell exactly once, achieving **O(m*n)** time.
      wrong_approach: "Separate BFS from each 1 cell"
      correct_approach: "Multi-source BFS starting from all 0 cells"

    - title: Forgetting to Mark Visited Cells
      description: |
        If you don't track which cells have been processed, you might add the same cell to the queue multiple times, leading to incorrect results or infinite loops.

        Using the `dist` matrix itself handles this: a cell is "visited" when its distance is no longer infinity. Only unvisited cells with infinite distance are added to the queue.
      wrong_approach: "No visited tracking, cells re-added to queue"
      correct_approach: "Use distance matrix to track visited status"

    - title: Incorrect Neighbour Bounds
      description: |
        When exploring neighbours, forgetting to check matrix bounds causes index errors.

        Always verify `0 <= nr < m` and `0 <= nc < n` before accessing `mat[nr][nc]`.

  key_takeaways:
    - "**Multi-source BFS**: When finding shortest distances from multiple sources, start BFS from all sources simultaneously rather than running separate searches"
    - "**Reverse the search direction**: Instead of searching from each target to sources, search from sources to targets — often more efficient"
    - "**Level-by-level guarantee**: BFS processes cells in order of distance, so the first time you reach a cell is via the shortest path"
    - "**Related problems**: This pattern applies to problems like 'Map of Highest Peak', 'Walls and Gates', and 'Rotting Oranges'"

  time_complexity: "O(m * n). Each cell is visited exactly once during BFS."
  space_complexity: "O(m * n). We store the distance matrix and the BFS queue, which in the worst case contains all cells."

solutions:
  - approach_name: Multi-source BFS
    is_optimal: true
    code: |
      from collections import deque

      def update_matrix(mat: list[list[int]]) -> list[list[int]]:
          m, n = len(mat), len(mat[0])
          # Initialise distances: 0 for zeros, infinity for ones
          dist = [[0 if mat[i][j] == 0 else float('inf')
                   for j in range(n)] for i in range(m)]

          # Queue all zero cells as starting points
          queue = deque()
          for i in range(m):
              for j in range(n):
                  if mat[i][j] == 0:
                      queue.append((i, j))

          # Four directions: up, down, left, right
          directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]

          # BFS from all zeros simultaneously
          while queue:
              r, c = queue.popleft()
              for dr, dc in directions:
                  nr, nc = r + dr, c + dc
                  # Check bounds and if we found a shorter path
                  if 0 <= nr < m and 0 <= nc < n:
                      if dist[r][c] + 1 < dist[nr][nc]:
                          dist[nr][nc] = dist[r][c] + 1
                          queue.append((nr, nc))

          return dist
    explanation: |
      **Time Complexity:** O(m * n) — Each cell is enqueued and processed exactly once.

      **Space Complexity:** O(m * n) — For the distance matrix and BFS queue.

      We start BFS from all zero cells simultaneously. Since BFS explores level by level, each cell is reached via the shortest path from some zero. The first time we update a cell's distance is the final answer for that cell.

  - approach_name: Dynamic Programming (Two-Pass)
    is_optimal: true
    code: |
      def update_matrix(mat: list[list[int]]) -> list[list[int]]:
          m, n = len(mat), len(mat[0])
          # Use a large value instead of infinity for easier arithmetic
          MAX_DIST = m + n

          # Initialise: 0 for zeros, large value for ones
          dist = [[0 if mat[i][j] == 0 else MAX_DIST
                   for j in range(n)] for i in range(m)]

          # First pass: top-left to bottom-right
          # Check cells above and to the left
          for i in range(m):
              for j in range(n):
                  if i > 0:
                      dist[i][j] = min(dist[i][j], dist[i-1][j] + 1)
                  if j > 0:
                      dist[i][j] = min(dist[i][j], dist[i][j-1] + 1)

          # Second pass: bottom-right to top-left
          # Check cells below and to the right
          for i in range(m - 1, -1, -1):
              for j in range(n - 1, -1, -1):
                  if i < m - 1:
                      dist[i][j] = min(dist[i][j], dist[i+1][j] + 1)
                  if j < n - 1:
                      dist[i][j] = min(dist[i][j], dist[i][j+1] + 1)

          return dist
    explanation: |
      **Time Complexity:** O(m * n) — Two passes through the matrix.

      **Space Complexity:** O(m * n) — For the distance matrix (can be O(1) if modifying in-place is allowed).

      The DP approach works because the shortest path to any zero must come from one of four directions. The first pass propagates distances from top and left; the second pass propagates from bottom and right. After both passes, each cell has the minimum distance considering all four directions.
