cswork / 模板打开 500 路题单

第 14 站 · 图论

网格遍历与多源搜索

多源 BFS,一步扩一圈 O(行数 × 列数)

周赛难度分 难度分 1433–难度分 1967· 中位 难度分 1742(4/15 道题有 zerotrac 周赛分)

打开 500 路题单 · 第 14 站 · 15 题
先学
学完去
面试频次
16 家公司考过 · 面经出现 32 次Meta 7、Amazon 5、Uber 3、Bloomberg 2、DoorDash 2

先做这道

LC 733图像渲染简单
卡住再看

洪水填充练习访问标记,处理新旧颜色相同的情况。

要点

  1. 所有起点一开始就一起入队,相当于从一个虚拟的超级源点出发;BFS 的层数就是最短距离。
  2. 入队时就标记已访问,同一个格子不会被重复加入。
  3. 只问连通块(岛屿个数、面积)时 DFS、BFS 都可以;问最少步数必须用 BFS。

易错点

  • 出队时才标记,同一个格子会被多个邻居重复入队,大网格上会超时。
  • 一开始就没有新鲜橘子时答案是 0,既不是 -1,也不是层数。
  • 递归 DFS 在 1000×1000 的网格上会爆栈;大网格用 BFS 或显式栈。

看动画

一步一步播放这个模式在小例子上的执行过程。可以暂停、单步前进或后退,拖动进度条跳到任意一步。

交互演示 · 逐步观察

BFS · 网格最短路径

从左上角 S 到右下角 T,最少走几步?

S(0,0)
·(0,1)
·(0,2)
·(0,3)
·(0,4)
·(1,0)
■(1,1)
■(1,2)
·(1,3)
·(1,4)
·(2,0)
·(2,1)
■(2,2)
·(2,3)
·(2,4)
·(3,0)
·(3,1)
·(3,2)
·(3,3)
T(3,4)
当前处理已发现 / 已计算答案
队首 → 队尾
(0,0)
已发现
1
当前距离
—
终点距离
未到达

起点 S 入队,距离为 0。每一步只能上下左右移动,深色格不可通行。

始终成立队列按距离从小到大处理;每个格子第一次被发现时,其最短距离已经确定。

交互演示 · 逐步观察

DFS · 网格岛屿数量

上下左右相连的陆地算一座岛,一共有几座?

换个输入
1(0,0)
1(0,1)
~(0,2)
~(0,3)
1(0,4)
~(0,5)
1(1,0)
~(1,1)
~(1,2)
1(1,3)
1(1,4)
~(1,5)
~(2,0)
~(2,1)
1(2,2)
~(2,3)
~(2,4)
~(2,5)
1(3,0)
~(3,1)
1(3,2)
1(3,3)
~(3,4)
1(3,5)
当前处理已发现 / 已计算答案
栈底 → 栈顶
空
岛屿数
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 minutes
C++
// 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, best
C++
// {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 -1
C++
// 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
}

为什么对

  1. BFS 按距离分层出队:距离为 d 的格子全部出队后,距离 d + 1 的才开始出队,所以一个格子第一次被发现时所在的层数就是最短步数。
  2. 多源 BFS 等价于从一个虚拟超级源点出发,它到每个真实源点的边权为 0;所以把所有源点一起放进第 0 层,算出的就是到最近源点的距离。
  3. 入队时就标记已访问:一个格子可能被同一层的多个邻居发现,晚标记会让它重复入队,答案不变,但最坏会多花很多时间和内存。

30 秒口述稿

面试官让你讲思路时,按做法、为什么对、复杂度的顺序说。中英文各一版,可以对着读几遍。

中文
网格最短步数我用 BFS,因为 BFS 按层扩展,一个格子第一次被发现时的层数就是最短距离。多个起点就一开始把所有起点一起入队,相当于从一个虚拟超级源点出发,算出的是到最近起点的距离。格子入队时就标记已访问,避免重复入队。每个格子最多入队一次,时间是行数乘列数。
English
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 天的间隔回来,到期的题在「我的进度 · 到期复习」里。 只存在这台设备上;登录后会合并到你的账号。

对照要点(0/3)
这道题你会了吗?

小测

4 道概念题和复杂度题。答过的题会和 OA 选择题练习一起按间隔回来复习。 去 OA 选择题练习

4 道题
  1. 在无权网格上求从起点到终点的最少步数,哪种搜索直接给出答案?
  2. BFS 为什么通常在把格子加入队列时就标记为已访问,而不是出队时才标记?
  3. R×C 网格里有 k 个源点,要求每个格子到最近源点的距离。高效的做法是?
  4. 网格是 R 行 C 列。时间复杂度是?
    from collections import deque
    
    def steps(grid, start):
        R, C = len(grid), len(grid[0])
        dist = {start: 0}
        q = deque([start])
        while q:
            r, c = q.popleft()
            for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
                if 0 <= nr < R and 0 <= nc < C and grid[nr][nc] == '.' and (nr, nc) not in dist:
                    dist[(nr, nc)] = dist[(r, c)] + 1
                    q.append((nr, nc))
        return dist

作答记录只保存在这台设备的浏览器里;登录后打开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

#题目一句话思路难度分
733图像渲染洪水填充练习访问标记,处理新旧颜色相同的情况。
200岛屿数量把相邻陆地视为连通块,掌握DFS/BFS访问标记。
695岛屿的最大面积在连通块遍历中累计面积并维护最大值。
463岛屿的周长用四邻接关系统计边界贡献,明确网格外部和水域。

核心 8

#题目一句话思路难度分
130被围绕的区域从边界反向标记安全区域,避免逐区域猜测是否被包围。
417太平洋大西洋水流问题从两片海洋逆向搜索可达区域,再取交集。
994腐烂的橘子多源BFS按分钟同时扩散,处理不可达的新鲜橘子。难度分 1433
286墙与门 🔒从全部门同时出发填充最短距离,避免为每个房间单独搜索。
54201 矩阵多源BFS计算每个格子到最近零的距离。
1091二进制矩阵中的最短路径在八邻接网格上寻找最短路径,处理起终点阻塞。难度分 1658
934最短的桥先标记一座岛再向外BFS,寻找连接两岛的最少翻转数。难度分 1826
490迷宫 🔒把球停下的位置作为图节点,而不是把经过的每格都当作状态。

进阶 3

#题目一句话思路难度分
329矩阵中的最长递增路径利用严格递增关系形成无环依赖,以记忆化搜索或拓扑剥层求最长路径,避免从每格重复搜索。
505迷宫 II 🔒滚动距离作为边权,使用最短路而非普通层数BFS。
1293网格中的最短路径在位置之外记录剩余消障次数,理解带资源状态的最短路径。难度分 1967

Core Skills

不调库,亲手实现这一站用到的数据结构或算法,四种语言都能判题。