第 14 站 · 图论
网格遍历与多源搜索
多源 BFS,一步扩一圈 O(行数 × 列数)
- 先学
先做这道
LC 733图像渲染卡住再看
洪水填充练习访问标记,处理新旧颜色相同的情况。
要点
- 所有起点一开始就一起入队,相当于从一个虚拟的超级源点出发;BFS 的层数就是最短距离。
- 入队时就标记已访问,同一个格子不会被重复加入。
- 只问连通块(岛屿个数、面积)时 DFS、BFS 都可以;问最少步数必须用 BFS。
易错点
- 出队时才标记,同一个格子会被多个邻居重复入队,大网格上会超时。
- 一开始就没有新鲜橘子时答案是 0,既不是 -1,也不是层数。
- 递归 DFS 在 1000×1000 的网格上会爆栈;大网格用 BFS 或显式栈。
看动画
一步一步播放这个模式在小例子上的执行过程。可以暂停、单步前进或后退,拖动进度条跳到任意一步。
BFS · 网格最短路径
从左上角 S 到右下角 T,最少走几步?
- 已发现
- 1
- 当前距离
- —
- 终点距离
- 未到达
起点 S 入队,距离为 0。每一步只能上下左右移动,深色格不可通行。
始终成立队列按距离从小到大处理;每个格子第一次被发现时,其最短距离已经确定。
DFS · 网格岛屿数量
上下左右相连的陆地算一座岛,一共有几座?
换个输入
- 岛屿数
- 0
- 已访问陆地
- 0
1 是陆地,~ 是水。逐行扫描,遇到没访问过的陆地就从它开始深搜。
始终成立压栈时就标记:每块陆地只入栈一次,所以总时间是 O(行 × 列)。
模板
选一种语言展开。四种写法实现的是同一个模板,每一份都编译运行过,并用测试数据核对了输出。看完用它重写热身题 LC 733。
Python
from collections import deque
def minutes_to_rot(grid: list[list[int]]) -> int:
"""0 empty, 1 fresh, 2 rotten. Minutes until nothing is fresh, or -1."""
rows, cols = len(grid), len(grid[0])
q = deque((r, c) for r in range(rows) for c in range(cols) if grid[r][c] == 2)
fresh = sum(row.count(1) for row in grid)
minutes = 0
while q and fresh:
minutes += 1
for _ in range(len(q)): # every rotten cell spreads one step this minute
r, c = q.popleft()
for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
grid[nr][nc] = 2 # mark when enqueued, not when popped
fresh -= 1
q.append((nr, nc))
return -1 if fresh else minutesC++
// 0 empty, 1 fresh, 2 rotten. Minutes until nothing is fresh, or -1.
int minutesToRot(vector<vector<int>> grid) {
int rows = grid.size(), cols = grid[0].size(), fresh = 0, minutes = 0;
queue<pair<int, int>> q;
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++) {
if (grid[r][c] == 2) q.push({r, c});
if (grid[r][c] == 1) fresh++;
}
const int dr[] = {1, -1, 0, 0}, dc[] = {0, 0, 1, -1};
while (!q.empty() && fresh) {
minutes++;
for (int k = q.size(); k > 0; k--) { // every rotten cell spreads one step this minute
auto [r, c] = q.front();
q.pop();
for (int d = 0; d < 4; d++) {
int nr = r + dr[d], nc = c + dc[d];
if (nr < 0 || nr >= rows || nc < 0 || nc >= cols || grid[nr][nc] != 1) continue;
grid[nr][nc] = 2; // mark when enqueued, not when popped
fresh--;
q.push({nr, nc});
}
}
}
return fresh ? -1 : minutes;
}Java
class Solution {
// 0 empty, 1 fresh, 2 rotten. Minutes until nothing is fresh, or -1.
int minutesToRot(int[][] grid) {
int rows = grid.length, cols = grid[0].length, fresh = 0, minutes = 0;
Deque<int[]> q = new ArrayDeque<>();
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++) {
if (grid[r][c] == 2) q.offer(new int[] {r, c});
if (grid[r][c] == 1) fresh++;
}
int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
while (!q.isEmpty() && fresh > 0) {
minutes++;
for (int k = q.size(); k > 0; k--) { // every rotten cell spreads one step this minute
int[] cell = q.poll();
for (int[] d : dirs) {
int nr = cell[0] + d[0], nc = cell[1] + d[1];
if (nr < 0 || nr >= rows || nc < 0 || nc >= cols || grid[nr][nc] != 1) continue;
grid[nr][nc] = 2; // mark when enqueued, not when popped
fresh--;
q.offer(new int[] {nr, nc});
}
}
}
return fresh > 0 ? -1 : minutes;
}
}Go
// minutesToRot takes 0 empty, 1 fresh, 2 rotten and returns the minutes until
// nothing is fresh, or -1.
func minutesToRot(grid [][]int) int {
type cell struct{ r, c int }
rows, cols := len(grid), len(grid[0])
var q []cell
fresh := 0
for r := range grid {
for c, v := range grid[r] {
switch v {
case 2:
q = append(q, cell{r, c})
case 1:
fresh++
}
}
}
dirs := []cell{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}
minutes := 0
for len(q) > 0 && fresh > 0 {
minutes++
var next []cell // every rotten cell spreads one step this minute
for _, p := range q {
for _, d := range dirs {
nr, nc := p.r+d.r, p.c+d.c
if nr < 0 || nr >= rows || nc < 0 || nc >= cols || grid[nr][nc] != 1 {
continue
}
grid[nr][nc] = 2 // mark when enqueued, not when popped
fresh--
next = append(next, cell{nr, nc})
}
}
q = next
}
if fresh > 0 {
return -1
}
return minutes
}常见变体
这一站的题还会用到下面 2 种写法。展开看什么时候换用它,代码同样四种语言都跑过测试。
显式栈 DFS 洪水填充时间 O(rows × cols) · 空间 O(rows × cols)
数连通块、算面积、给一块区域换颜色:从一个没访问过的格子出发,把四个方向上同类的格子都标掉。用显式栈代替递归,大岛屿不会爆栈;入栈时就标记,同一格不会进栈两次。LC 200 岛屿数量、LC 695 岛屿的最大面积、LC 733 图像渲染。
Python
def island_stats(grid: list[list[int]]) -> tuple[int, int]:
"""(number of islands, largest island area) in a 0/1 grid (LC 200, LC 695)."""
rows, cols = len(grid), len(grid[0])
seen = [[False] * cols for _ in range(rows)]
count = best = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] != 1 or seen[r][c]:
continue
count += 1
area, stack = 0, [(r, c)]
seen[r][c] = True
while stack: # an explicit stack: no recursion limit on big islands
i, j = stack.pop()
area += 1
for ni, nj in ((i + 1, j), (i - 1, j), (i, j + 1), (i, j - 1)):
if 0 <= ni < rows and 0 <= nj < cols and grid[ni][nj] == 1 and not seen[ni][nj]:
seen[ni][nj] = True # mark on push: no cell goes in twice
stack.append((ni, nj))
best = max(best, area)
return count, bestC++
// {number of islands, largest island area} in a 0/1 grid (LC 200, LC 695).
pair<int, int> islandStats(const vector<vector<int>>& grid) {
int rows = grid.size(), cols = grid[0].size(), count = 0, best = 0;
vector<vector<bool>> seen(rows, vector<bool>(cols, false));
int dr[] = {1, -1, 0, 0}, dc[] = {0, 0, 1, -1};
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++) {
if (grid[r][c] != 1 || seen[r][c]) continue;
count++;
int area = 0;
vector<pair<int, int>> stack{{r, c}};
seen[r][c] = true;
while (!stack.empty()) { // an explicit stack: no recursion limit on big islands
auto [i, j] = stack.back();
stack.pop_back();
area++;
for (int d = 0; d < 4; d++) {
int ni = i + dr[d], nj = j + dc[d];
if (ni >= 0 && ni < rows && nj >= 0 && nj < cols && grid[ni][nj] == 1 && !seen[ni][nj]) {
seen[ni][nj] = true; // mark on push: no cell goes in twice
stack.push_back({ni, nj});
}
}
}
best = max(best, area);
}
return {count, best};
}Java
class Solution {
// {number of islands, largest island area} in a 0/1 grid (LC 200, LC 695).
int[] islandStats(int[][] grid) {
int rows = grid.length, cols = grid[0].length, count = 0, best = 0;
boolean[][] seen = new boolean[rows][cols];
int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++) {
if (grid[r][c] != 1 || seen[r][c]) continue;
count++;
int area = 0;
Deque<int[]> stack = new ArrayDeque<>();
stack.push(new int[] {r, c});
seen[r][c] = true;
while (!stack.isEmpty()) { // an explicit stack: no recursion limit on big islands
int[] cell = stack.pop();
area++;
for (int[] d : dirs) {
int ni = cell[0] + d[0], nj = cell[1] + d[1];
if (ni >= 0 && ni < rows && nj >= 0 && nj < cols && grid[ni][nj] == 1 && !seen[ni][nj]) {
seen[ni][nj] = true; // mark on push: no cell goes in twice
stack.push(new int[] {ni, nj});
}
}
}
best = Math.max(best, area);
}
return new int[] {count, best};
}
}Go
// islandStats returns the number of islands and the largest island area in a 0/1 grid.
func islandStats(grid [][]int) (int, int) {
rows, cols := len(grid), len(grid[0])
seen := make([][]bool, rows)
for r := range seen {
seen[r] = make([]bool, cols)
}
count, best := 0, 0
dirs := [][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}
for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ {
if grid[r][c] != 1 || seen[r][c] {
continue
}
count++
area := 0
stack := [][2]int{{r, c}}
seen[r][c] = true
for len(stack) > 0 { // an explicit stack: no recursion limit on big islands
cell := stack[len(stack)-1]
stack = stack[:len(stack)-1]
area++
for _, d := range dirs {
ni, nj := cell[0]+d[0], cell[1]+d[1]
if ni >= 0 && ni < rows && nj >= 0 && nj < cols && grid[ni][nj] == 1 && !seen[ni][nj] {
seen[ni][nj] = true // mark on push: no cell goes in twice
stack = append(stack, [2]int{ni, nj})
}
}
}
best = max(best, area)
}
}
return count, best
}带状态的网格 BFS:(行, 列, 剩余次数)时间 O(rows × cols × k) · 空间 O(rows × cols × k)
走格子时还带着别的资源(还能打穿几堵墙、手里有哪些钥匙):状态变成 (r, c, 资源),按层 BFS 仍然给出最少步数。同一格只在「剩余资源比以前多」时才值得再进队,这一条剪枝让状态数降下来。LC 1293 网格中的最短路径。
Python
from collections import deque
def shortest_with_breaks(grid: list[list[int]], k: int) -> int:
"""Fewest steps from top-left to bottom-right, breaking at most k walls (LC 1293)."""
rows, cols = len(grid), len(grid[0])
best = [[-1] * cols for _ in range(rows)] # most breaks still in hand on reaching a cell
best[0][0] = k
q = deque([(0, 0, k)]) # a state is (r, c, breaks left)
steps = 0
while q:
for _ in range(len(q)): # one BFS level per step
r, c, left = q.popleft()
if r == rows - 1 and c == cols - 1:
return steps
for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
if 0 <= nr < rows and 0 <= nc < cols:
rest = left - grid[nr][nc]
if rest > best[nr][nc]: # more breaks left than any earlier visit: worth it
best[nr][nc] = rest
q.append((nr, nc, rest))
steps += 1
return -1C++
// Fewest steps from top-left to bottom-right, breaking at most k walls (LC 1293).
int shortestWithBreaks(const vector<vector<int>>& grid, int k) {
int rows = grid.size(), cols = grid[0].size();
vector<vector<int>> best(rows, vector<int>(cols, -1)); // most breaks still in hand on reaching a cell
best[0][0] = k;
queue<array<int, 3>> q; // a state is {r, c, breaks left}
q.push({0, 0, k});
int dr[] = {1, -1, 0, 0}, dc[] = {0, 0, 1, -1};
for (int steps = 0; !q.empty(); steps++)
for (int n = q.size(); n > 0; n--) { // one BFS level per step
auto [r, c, left] = q.front();
q.pop();
if (r == rows - 1 && c == cols - 1) return steps;
for (int d = 0; d < 4; d++) {
int nr = r + dr[d], nc = c + dc[d];
if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;
int rest = left - grid[nr][nc];
if (rest > best[nr][nc]) { // more breaks left than any earlier visit: worth it
best[nr][nc] = rest;
q.push({nr, nc, rest});
}
}
}
return -1;
}Java
class Solution {
// Fewest steps from top-left to bottom-right, breaking at most k walls (LC 1293).
int shortestWithBreaks(int[][] grid, int k) {
int rows = grid.length, cols = grid[0].length;
int[][] best = new int[rows][cols]; // most breaks still in hand on reaching a cell
for (int[] row : best) Arrays.fill(row, -1);
best[0][0] = k;
Deque<int[]> q = new ArrayDeque<>(); // a state is {r, c, breaks left}
q.offer(new int[] {0, 0, k});
int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (int steps = 0; !q.isEmpty(); steps++)
for (int n = q.size(); n > 0; n--) { // one BFS level per step
int[] s = q.poll();
if (s[0] == rows - 1 && s[1] == cols - 1) return steps;
for (int[] d : dirs) {
int nr = s[0] + d[0], nc = s[1] + d[1];
if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;
int rest = s[2] - grid[nr][nc];
if (rest > best[nr][nc]) { // more breaks left than any earlier visit: worth it
best[nr][nc] = rest;
q.offer(new int[] {nr, nc, rest});
}
}
}
return -1;
}
}Go
// shortestWithBreaks returns the fewest steps from top-left to bottom-right, breaking at most k walls.
func shortestWithBreaks(grid [][]int, k int) int {
rows, cols := len(grid), len(grid[0])
best := make([][]int, rows) // most breaks still in hand on reaching a cell
for r := range best {
best[r] = make([]int, cols)
for c := range best[r] {
best[r][c] = -1
}
}
best[0][0] = k
q := [][3]int{{0, 0, k}} // a state is {r, c, breaks left}
dirs := [][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}
for steps := 0; len(q) > 0; steps++ {
var next [][3]int // the following BFS level
for _, s := range q {
if s[0] == rows-1 && s[1] == cols-1 {
return steps
}
for _, d := range dirs {
nr, nc := s[0]+d[0], s[1]+d[1]
if nr < 0 || nr >= rows || nc < 0 || nc >= cols {
continue
}
if rest := s[2] - grid[nr][nc]; rest > best[nr][nc] { // more breaks left than before: worth it
best[nr][nc] = rest
next = append(next, [3]int{nr, nc, rest})
}
}
}
q = next
}
return -1
}为什么对
- BFS 按距离分层出队:距离为 d 的格子全部出队后,距离 d + 1 的才开始出队,所以一个格子第一次被发现时所在的层数就是最短步数。
- 多源 BFS 等价于从一个虚拟超级源点出发,它到每个真实源点的边权为 0;所以把所有源点一起放进第 0 层,算出的就是到最近源点的距离。
- 入队时就标记已访问:一个格子可能被同一层的多个邻居发现,晚标记会让它重复入队,答案不变,但最坏会多花很多时间和内存。
30 秒口述稿
面试官让你讲思路时,按做法、为什么对、复杂度的顺序说。中英文各一版,可以对着读几遍。
网格最短步数我用 BFS,因为 BFS 按层扩展,一个格子第一次被发现时的层数就是最短距离。多个起点就一开始把所有起点一起入队,相当于从一个虚拟超级源点出发,算出的是到最近起点的距离。格子入队时就标记已访问,避免重复入队。每个格子最多入队一次,时间是行数乘列数。For the fewest steps on a grid I use BFS, since it expands layer by layer and the layer where a cell is first found is its shortest distance. With several starting points I enqueue all of them at the start, which is like one virtual super source, and I get the distance to the nearest source. I mark a cell visited when I enqueue it so it never enters the queue twice. Each cell is enqueued at most once, so the cost is rows times columns.自评一下
对照上面的答案给自己打个分。没掌握的题会按 1、3、7、21 天的间隔回来,到期的题在「我的进度 · 到期复习」里。 只存在这台设备上;登录后会合并到你的账号。
小测
4 道概念题和复杂度题。答过的题会和 OA 选择题练习一起按间隔回来复习。 去 OA 选择题练习
作答记录只保存在这台设备的浏览器里;登录后打开OA 选择题练习页,会存进账号。
模板练习
用上面的模板和变体出题:把打乱的行排回原序,把挖掉的边界条件填回去,或者在空白编辑器里整段默写,跑本站的测试并逐行对比。每次「换一题」都会重新出题。
下面几行被打乱了。点一行选中,再点另一行和它交换,排回模板原来的顺序;缩进是提示。
from collections import deque
def minutes_to_rot(grid: list[list[int]]) -> int:
"""0 empty, 1 fresh, 2 rotten. Minutes until nothing is fresh, or -1.""" q.append((nr, nc))
return -1 if fresh else minutes分档练习
本站在 500 路里的 15 道题,按基础、核心、进阶分三档。每题附一句话思路,先自己想,想不出再看。
基础 4
核心 8
| # | 题目 | 一句话思路 | 难度分 |
|---|---|---|---|
| 130 | 被围绕的区域 | 从边界反向标记安全区域,避免逐区域猜测是否被包围。 | |
| 417 | 太平洋大西洋水流问题 | 从两片海洋逆向搜索可达区域,再取交集。 | |
| 994 | 腐烂的橘子 | 多源BFS按分钟同时扩散,处理不可达的新鲜橘子。 | |
| 286 | 墙与门 🔒 | 从全部门同时出发填充最短距离,避免为每个房间单独搜索。 | |
| 542 | 01 矩阵 | 多源BFS计算每个格子到最近零的距离。 | |
| 1091 | 二进制矩阵中的最短路径 | 在八邻接网格上寻找最短路径,处理起终点阻塞。 | |
| 934 | 最短的桥 | 先标记一座岛再向外BFS,寻找连接两岛的最少翻转数。 | |
| 490 | 迷宫 🔒 | 把球停下的位置作为图节点,而不是把经过的每格都当作状态。 |
进阶 3
| # | 题目 | 一句话思路 | 难度分 |
|---|---|---|---|
| 329 | 矩阵中的最长递增路径 | 利用严格递增关系形成无环依赖,以记忆化搜索或拓扑剥层求最长路径,避免从每格重复搜索。 | |
| 505 | 迷宫 II 🔒 | 滚动距离作为边权,使用最短路而非普通层数BFS。 | |
| 1293 | 网格中的最短路径 | 在位置之外记录剩余消障次数,理解带资源状态的最短路径。 |
本站 OA 真题 21
- Amazon Fresh — Delivery Grid BFSAmazon
- Find Suitable LocationsMicrosoft
- Cleaning RobotMicrosoft
- Island Perimeter (Round 3, LC 463 :)Microsoft
- Matching Regions in Two GridsOpenAI
- Grid Infection Spread Until StableOpenAI
- Grid Infection with Immune Cells Until StableOpenAI
- Grid TraversalSnowflake
- Replacing ValTwo Sigma
- Simulate Water FlowUber