209 lines
9.3 KiB
YAML
209 lines
9.3 KiB
YAML
title: 01 Matrix
|
|
slug: 01-matrix
|
|
difficulty: medium
|
|
leetcode_id: 542
|
|
leetcode_url: https://leetcode.com/problems/01-matrix/
|
|
categories:
|
|
- arrays
|
|
- graphs
|
|
patterns:
|
|
- slug: bfs
|
|
is_optimal: true
|
|
- slug: matrix-traversal
|
|
is_optimal: false
|
|
- slug: dynamic-programming
|
|
is_optimal: false
|
|
|
|
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`
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
|
|
**Step 3: Return the result**
|
|
|
|
- Return the `dist` matrix after BFS completes
|
|
|
|
|
|
|
|
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.
|