questions A (01-matrix - avoid-flood)
This commit is contained in:
185
backend/data/questions/01-matrix.yaml
Normal file
185
backend/data/questions/01-matrix.yaml
Normal file
@@ -0,0 +1,185 @@
|
||||
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
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user