登录
OAmaster
网格图

网格图 · 多源 BFS

从 LC 994 腐烂的橘子开始:所有起点一次性入队,BFS 按"距离层"扩张,O(mn) 求出到任意起点的最短距离。

Medium预计阅读 50 分钟更新于 2026-05-18

1. 核心原理

多源 BFS(multi-source BFS)是单源 BFS 的推广:单源 BFS 从一个起点 s 出发求 s 到所有可达点的最短距离;多源 BFS 输入一组起点 S = {s₁, s₂, …, sₖ},求每个点 v最近某个起点的距离。

形式上要计算的是:

dist(v) = min over s in S of bfs_dist(s, v)

朴素做法对每个起点跑一遍 BFS,O(k · mn),当 k 很大时不可行。多源 BFS 把这件事压缩到 O(mn)

等价构造:虚拟超级源点

多源 BFS 的标准解释:新建一个虚拟节点 s*,向所有 sᵢ 连一条长度为 0 的边——从 s* 出发跑一次单源 BFS,得到的 dist(s*, v) 就等于 min over sᵢ of dist(sᵢ, v)

            s*  (虚拟超级源点)
           /|\ \
          0 0 0 0   (零费用边)
          | | | |
          s₁ s₂ s₃ s₄  (真实起点)

代码上不必真的建虚拟节点。直接把所有 sᵢ 一次性塞进队列、距离都记 0 就行——队列里的"层"天然就是距离层,和先经过 s* 完全等价。

关键不变量

BFS 队列里始终保持"待扩张层"的所有节点,且这些节点到(最近的)起点的距离相同。出队顺序按距离非递减。

这条不变量保证了"出队即定距"——一个节点第一次入队时记下的距离就是最终答案,后续不再更新。无权图(每步代价 1)的 BFS 正确性都建立在这条不变量上。

单源 vs 多源 对照

单源 BFS多源 BFS
起点数1k(k ≥ 1)
队列初始[s][s₁, s₂, …, sₖ]
距离初始化dist[s] = 0,其他 所有 sᵢdist[sᵢ] = 0,其他
答案语义从 s 到 v 的最短距离v 到最近某个起点的最短距离
复杂度O(V + E)O(V + E)(k 不影响渐进)

第三行很关键——多源初始化时所有起点距离都是 0,不是按"先后顺序"递增。这跟"虚拟超级源点零费用接到所有 sᵢ"的几何含义一致。

适用场景

只要题目问的是"网格(或一般图)上每个点到最近的某类目标的距离",且每一步代价是 1(无权图),就是多源 BFS。本章三道例题分别是:

  • LC 994 腐烂的橘子:起点是所有腐烂橘子,求最远新鲜橘子的距离。
  • LC 542 01 矩阵:起点是所有 0,求每个 1 到最近 0 的距离矩阵。
  • LC 1162 离海洋最远的陆地:起点是所有陆地,求所有海洋格子里到最近陆地距离的最大值。

不适用的两类情况:

  • 每步代价不全是 1(带权图)→ 上 Dijkstra 或 0-1 BFS。
  • 要求"从一个起点到目标的路径"而非"任一起点到该点的距离" → 仍是单源 BFS。

2. 通用模板

下面是多源 BFS 的"求网格上每个点到最近起点距离"骨架。返回二维 dist 数组:dist[i][j](i, j) 到最近某个起点的距离,不可达保留 -1

from collections import deque

def multi_source_bfs(grid: list[list[int]], is_source, can_walk) -> list[list[int]]:
    """
    多源 BFS 通用骨架。
    is_source(grid, i, j):(i, j) 是否是起点
    can_walk(grid, i, j):(i, j) 是否可走(非障碍)
    返回 dist 二维数组;不可达记 -1。
    """
    m, n = len(grid), len(grid[0])
    dist = [[-1] * n for _ in range(m)]
    q = deque()
    # 1. 第一遍扫整张网格:所有起点一次性入队,距离 0
    for i in range(m):
        for j in range(n):
            if is_source(grid, i, j):
                dist[i][j] = 0
                q.append((i, j))
    # 2. 标准 BFS 按层扩张
    while q:
        i, j = q.popleft()
        for di, dj in ((1, 0), (-1, 0), (0, 1), (0, -1)):
            ni, nj = i + di, j + dj
            if (0 <= ni < m and 0 <= nj < n
                    and dist[ni][nj] == -1                # 3. 入队即定距,未访问才进
                    and can_walk(grid, ni, nj)):
                dist[ni][nj] = dist[i][j] + 1             # 4. 邻居距离 = 当前 + 1
                q.append((ni, nj))
    return dist
import java.util.*;

public int[][] multiSourceBFS(int[][] grid,
                              java.util.function.BiPredicate<Integer, Integer> isSource,
                              java.util.function.BiPredicate<Integer, Integer> canWalk) {
    int m = grid.length, n = grid[0].length;
    int[][] dist = new int[m][n];
    for (int[] row : dist) Arrays.fill(row, -1);
    Deque<int[]> q = new ArrayDeque<>();
    // 1. 起点一次性入队
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (isSource.test(i, j)) {
                dist[i][j] = 0;
                q.offer(new int[]{i, j});
            }
        }
    }
    int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
    // 2. 标准 BFS 按层扩张
    while (!q.isEmpty()) {
        int[] cur = q.poll();
        int i = cur[0], j = cur[1];
        for (int[] d : dirs) {
            int ni = i + d[0], nj = j + d[1];
            if (ni >= 0 && ni < m && nj >= 0 && nj < n
                    && dist[ni][nj] == -1                  // 3. 入队即定距
                    && canWalk.test(ni, nj)) {
                dist[ni][nj] = dist[i][j] + 1;             // 4. 邻居距离 = 当前 + 1
                q.offer(new int[]{ni, nj});
            }
        }
    }
    return dist;
}
vector<vector<int>> multiSourceBFS(vector<vector<int>>& grid,
                                   function<bool(int, int)> isSource,
                                   function<bool(int, int)> canWalk) {
    int m = grid.size(), n = grid[0].size();
    vector<vector<int>> dist(m, vector<int>(n, -1));
    queue<pair<int, int>> q;
    // 1. 起点一次性入队
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (isSource(i, j)) {
                dist[i][j] = 0;
                q.push({i, j});
            }
        }
    }
    int dirs[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
    // 2. 标准 BFS 按层扩张
    while (!q.empty()) {
        auto [i, j] = q.front();
        q.pop();
        for (auto& d : dirs) {
            int ni = i + d[0], nj = j + d[1];
            if (ni >= 0 && ni < m && nj >= 0 && nj < n &&
                dist[ni][nj] == -1 &&                       // 3. 入队即定距
                canWalk(ni, nj)) {
                dist[ni][nj] = dist[i][j] + 1;              // 4. 邻居距离 = 当前 + 1
                q.push({ni, nj});
            }
        }
    }
    return dist;
}

复杂度:时间 O(mn),每个格子至多入队一次出队一次;空间 O(mn),dist 矩阵 + 队列最坏装下整张网格。

模板要点:

  • 入队即定距:一个格子第一次入队就记下最终距离,后面再也不更新。判断"是否已访问"用 dist[ni][nj] == -1(−1 当哨兵),不需要单独的 visited 数组。
  • 两层初始化扫描 O(mn) + BFS O(mn),总 O(mn),与起点数量 k 无关。
  • is_sourcecan_walk 分开:起点也是"可走"的,但不是所有"可走"的都是起点。比如 LC 994 起点是 grid == 2,可走是 grid != 0(空格不可走)。
  • 染色时机在入队前q.append((ni, nj)) 之前先 dist[ni][nj] = dist[i][j] + 1,避免同一格被多次入队。

3. 母题精讲

LeetCode 994. Rotting Oranges

链接:LeetCode 994

题意:给一个 m × n 的网格 grid,每一格的值是下面三个之一:

  • 0 空格
  • 1 新鲜橘子
  • 2 腐烂橘子

每过 1 分钟,所有腐烂橘子会让它上下左右相邻的新鲜橘子也变腐烂。返回让所有新鲜橘子都变腐烂所需的最少分钟数;如果有新鲜橘子永远不会腐烂,返回 −1。

数据范围:

  • m, n:1 到 10
  • grid[i][j]:0、1 或 2

示例:

输入:grid = [
  [2, 1, 1],
  [1, 1, 0],
  [0, 1, 1]
]
输出:4
解释:
分钟 0  分钟 1  分钟 2  分钟 3  分钟 4
2 1 1   2 2 1   2 2 2   2 2 2   2 2 2
1 1 0   2 1 0   2 2 0   2 2 0   2 2 0
0 1 1   0 1 1   0 1 1   0 2 1   0 2 2

暴力解:每分钟扫一遍整个网格

最直觉的写法是按时间步进——每一分钟扫一遍整张网格,找出"上一分钟刚变腐烂"的橘子,让它们的相邻新鲜橘子在下一分钟腐烂。重复直到没有新增腐烂为止:

class Solution:
    def orangesRotting_brute(self, grid: list[list[int]]) -> int:
        m, n = len(grid), len(grid[0])
        minutes = 0
        while True:
            to_rot = []
            # 整张网格扫一遍找"上一分钟刚腐烂"的橘子,让相邻新鲜橘子变 2
            for i in range(m):
                for j in range(n):
                    if grid[i][j] == 2:
                        for di, dj in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                            ni, nj = i + di, j + dj
                            if 0 <= ni < m and 0 <= nj < n and grid[ni][nj] == 1:
                                to_rot.append((ni, nj))
            if not to_rot:
                break
            for i, j in to_rot:
                grid[i][j] = 2
            minutes += 1
        # 还有新鲜橘子说明腐烂不完
        for row in grid:
            if 1 in row:
                return -1
        return minutes

这份代码思路清晰,但有两个明显的浪费:

第一,每一分钟都扫一遍整张网格。设答案是 T 分钟,整体复杂度 O(T · mn)m, n ≤ 10 时 T 最多 100,看起来不大,但同样的题型放到 LC 542(数据范围 10⁴ × 10⁴)就直接爆。

第二,已经腐烂的橘子会被反复扫描。一个橘子在第 1 分钟腐烂后,第 2、3、4 分钟还在被一遍遍扫,每次只是为了确认"它已经腐烂了"——这部分计算完全没必要。

更深一层的浪费在于:每个腐烂橘子并不需要知道当前是第几分钟,它只需要知道"我邻居里有哪些是新鲜的,给它们打上比我大 1 的时间戳"。这是 BFS 的天然模式——按层扩张。

关键观察:所有腐烂橘子在第 0 分钟同时入队

把"多个起点同时扩散"翻译成 BFS:

  • 第 0 步:把所有腐烂橘子的坐标一次性塞进队列,距离记 0。
  • 后续步:标准 BFS——出队、看四邻居、新鲜的入队并标记距离为当前 + 1。
  • 队列空了之后,扫一遍网格,如果还有 1 就返回 −1,否则返回最大距离。

每个橘子至多入队一次、出队一次,整体 O(mn),跟初始腐烂橘子的数量无关——这就是 §1 多源 BFS 的虚拟超级源点等价构造。

下面这个交互演示走一遍示例输入 [[2,1,1],[1,1,0],[0,1,1]] 的 BFS 过程(网格行优先展开成 9 格:(0,0)→idx 0,(0,1)→idx 1,… (2,2)→idx 8):

orangesRotting 多源 BFS 按层扩张

网格 [[2,1,1],[1,1,0],[0,1,1]] 行优先展开。值:2=已腐烂,1=新鲜,0=空。颜色:红=队首正在扩张,金=本层新入队,蓝=已处理

Step 1 / 7分钟 0:扫整张网格,把所有 2 一次性入队。当前只有 (0,0)=idx 0 是腐烂的。队列:[(0,0,0)],距离 0
index
012345678
value
2
1
1
1
1
0
0
1
1
queue head
队首/本层起点本层新入队已腐烂(出队完毕)新鲜或空格,未入队
01/07

m × n ≤ 100 这道题感觉不出渐进区别,但同样的"多源同时启动"骨架放到 §4.2 LC 542 / §4.3 LC 1162 这种 10⁴ × 10⁴ 网格上,暴力版直接 TLE。


4. 例题

下面三道按"目标和数据规模递增"排:LC 994 求最大距离(小网格) → LC 542 输出全距离矩阵(大网格) → LC 1162 求矩阵里距离的最大值。每道带 follow-up 续到下一题。

4.1 LC 994 腐烂的橘子回头看

题目链接。题面见 §3。直接套 §2 多源 BFS 模板——起点是所有 2,可走是所有 != 0。三语言完整实现:

from collections import deque

class Solution:
    def orangesRotting(self, grid: list[list[int]]) -> int:
        m, n = len(grid), len(grid[0])
        q = deque()
        fresh = 0
        # 第一遍扫一遍,腐烂橘子入队、统计新鲜橘子数
        for i in range(m):
            for j in range(n):
                if grid[i][j] == 2:
                    q.append((i, j, 0))                # (i, j, 当前分钟)
                elif grid[i][j] == 1:
                    fresh += 1

        minutes = 0
        while q:
            i, j, t = q.popleft()
            minutes = t
            for di, dj in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                ni, nj = i + di, j + dj
                if 0 <= ni < m and 0 <= nj < n and grid[ni][nj] == 1:
                    grid[ni][nj] = 2                   # 入队即染色
                    fresh -= 1
                    q.append((ni, nj, t + 1))

        return minutes if fresh == 0 else -1
class Solution {
    public int orangesRotting(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        Deque<int[]> q = new ArrayDeque<>();
        int fresh = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 2) q.offer(new int[]{i, j, 0});
                else if (grid[i][j] == 1) fresh++;
            }
        }
        int minutes = 0;
        int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
        while (!q.isEmpty()) {
            int[] cur = q.poll();
            int i = cur[0], j = cur[1], t = cur[2];
            minutes = t;
            for (int[] d : dirs) {
                int ni = i + d[0], nj = j + d[1];
                if (ni >= 0 && ni < m && nj >= 0 && nj < n && grid[ni][nj] == 1) {
                    grid[ni][nj] = 2;
                    fresh--;
                    q.offer(new int[]{ni, nj, t + 1});
                }
            }
        }
        return fresh == 0 ? minutes : -1;
    }
}
class Solution {
public:
    int orangesRotting(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        queue<tuple<int, int, int>> q;
        int fresh = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 2) q.push({i, j, 0});
                else if (grid[i][j] == 1) fresh++;
            }
        }
        int minutes = 0;
        int dirs[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
        while (!q.empty()) {
            auto [i, j, t] = q.front();
            q.pop();
            minutes = t;
            for (auto& d : dirs) {
                int ni = i + d[0], nj = j + d[1];
                if (ni >= 0 && ni < m && nj >= 0 && nj < n && grid[ni][nj] == 1) {
                    grid[ni][nj] = 2;                  // 入队即染色
                    fresh--;
                    q.push({ni, nj, t + 1});
                }
            }
        }
        return fresh == 0 ? minutes : -1;
    }
};

复杂度:时间 O(mn)、空间 O(mn)(队列最坏装下整张网格)。

实现里几个工程细节值得拎出来:

第一,fresh 计数器代替最后扫一遍。BFS 结束后判断有没有"漏网"的新鲜橘子,直接看 fresh == 0 比再扫一遍网格干净。每次有新鲜橘子被腐烂时 fresh -= 1,初始扫描时累计。

第二,入队即染色——grid[ni][nj] = 2q.append 之前完成。如果先 append 再染色,BFS 在多腐烂橘子相邻的情况下会让同一格被多次入队(最坏 4 次),复杂度退化。

第三,起点距离打成 0 而不是 1。一个常见 bug 是把初始腐烂橘子记距离 1,把 minutes = t - 1 来"修正"——这种打法 BFS 在第一层扩张时就会算错。更干净的做法是初始就 0,最后直接返回 t

第四,全无腐烂橘子但有新鲜橘子怎么办。队列空、fresh > 0,返回 −1,逻辑正确(minutes 保留为 0,但被 fresh != 0 拦住)。

第五,全无新鲜橘子(初始就赢)fresh = 0,BFS 队列里只有起点,扩张一圈都没东西可腐烂,minutes 保留为 0。但要小心:如果队列里所有元素 t = 0,循环里 minutes = t = 0,正确。

Follow-up:上面这题问的是"全部腐烂的总时间"——只需要最大距离,不在乎每个新鲜橘子被腐烂的精确时刻。如果题目反过来——给一个 0/1 矩阵,要求输出每个 1 到最近 0 的距离矩阵呢?


4.2 LC 542 01 矩阵

链接:LeetCode 542

题意:给一个 m × n 的 0/1 矩阵 mat。对每个 mat[i][j],求它到最近的 0 的曼哈顿距离(即网格距离,4-邻接)。返回同尺寸的距离矩阵。

数据范围:

  • m, n:1 到 10⁴
  • m × n ≤ 10⁴
  • mat[i][j]:0 或 1
  • 至少有一个 0

示例:

输入:mat = [
  [0, 0, 0],
  [0, 1, 0],
  [1, 1, 1]
]
输出:[
  [0, 0, 0],
  [0, 1, 0],
  [1, 2, 1]
]
解释:每个 1 到最近 0 的曼哈顿距离。比如 (2,2)=1,它的上邻居 (1,2)=0,距离 1;中心 (1,1)=1 四面都被 0 包围(取 (0,1)),距离 1;(2,1)=1 的最近 0 是 (1,0) 或 (0,1),距离 2。

思路上有两条主流路径:

思路 A:从每个 1 出发跑一次 BFS 找最近的 0——O((mn)²)mn ≤ 10⁴10⁸ 次操作,秒级 TLE。这是"单源 BFS 做 mn 遍"的暴力。

思路 B:从所有 0 出发的多源 BFS——O(mn)。把所有 0 一次性入队,BFS 自然按距离层扩张,碰到 1 时记下当前距离。这是本章主线。

思路 C:两遍 DP 扫描——O(mn)。第一遍从左上扫到右下、第二遍从右下扫到左上,每个点用左 / 上 / 右 / 下邻居的 dp + 1 更新自己。这也是 O(mn) 但常数稍小。多源 BFS 思路更通用、更易迁移到其他题(LC 1162),所以这里只展开 BFS 版。

from collections import deque

class Solution:
    def updateMatrix(self, mat: list[list[int]]) -> list[list[int]]:
        m, n = len(mat), len(mat[0])
        # dist[i][j] = -1 表示尚未确定距离;0 表示自己就是源
        dist = [[-1] * n for _ in range(m)]
        q = deque()
        # 第一步:所有 0 一次性入队,距离 0
        for i in range(m):
            for j in range(n):
                if mat[i][j] == 0:
                    dist[i][j] = 0
                    q.append((i, j))
        # 第二步:标准 BFS 按层扩张
        while q:
            i, j = q.popleft()
            for di, dj in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                ni, nj = i + di, j + dj
                if 0 <= ni < m and 0 <= nj < n and dist[ni][nj] == -1:
                    dist[ni][nj] = dist[i][j] + 1
                    q.append((ni, nj))
        return dist

复杂度:时间 O(mn)、空间 O(mn)(dist 矩阵 + 队列)。

跟 LC 994 的差异只有两点:

  • LC 994 起点是 2、扩张目标是 1;LC 542 起点是 0、扩张目标是 1。
  • LC 994 只关心"最远距离";LC 542 要输出整张距离矩阵。

骨架完全一致。这就是多源 BFS 的通用性——把"什么算起点、什么算可走"换一下,剩下都是模板。

为什么入队判断 dist[ni][nj] == -1 等价于 visited:BFS 第一次到达一个点时距离一定最短(无权图不变量),所以一旦距离不再是 −1,就不必再考虑了。这等于"入队即定距 + 之后不再访问"。

Follow-up:LC 542 要的是每个 1 到最近 0 的距离的整张矩阵。如果题目反过来——给一个 0/1 网格,0 是海洋、1 是陆地,要求海洋格子到最近陆地距离的最大值呢?


4.3 LC 1162 离海洋最远的陆地(题意按 LeetCode 原版)

链接:LeetCode 1162

题意:给一个 n × n 的 0/1 网格 grid0 表示海洋、1 表示陆地。在所有海洋格子中,找到离最近的陆地格子的曼哈顿距离最大的那个,返回这个最大值。如果整张图全是陆地或全是海洋(即没有可对比的情况),返回 −1。

数据范围:

  • n:1 到 100
  • grid[i][j]:0 或 1

示例:

输入:grid = [
  [1, 0, 1],
  [0, 0, 0],
  [1, 0, 1]
]
输出:2
解释:中心 (1,1) 是海洋,离最近陆地(四角)距离都是 2,所以答案 2。

思路:跟 LC 542 几乎一模一样——多源 BFS 从所有陆地出发,扩张时给每个海洋记下"到最近陆地的距离",最后取所有海洋格子里的最大距离。LC 542 输出整张矩阵、LC 1162 只要矩阵里的最大值。

from collections import deque

class Solution:
    def maxDistance(self, grid: list[list[int]]) -> int:
        n = len(grid)
        q = deque()
        # 所有陆地入队,距离 0
        for i in range(n):
            for j in range(n):
                if grid[i][j] == 1:
                    q.append((i, j, 0))
        # 全陆地 / 全海洋的退化情况,按题意返回 -1
        if not q or len(q) == n * n:
            return -1
        ans = 0
        # BFS,沿途记录到达海洋时的最大距离
        # 用 grid 自己标记 visited(陆地 1,访问过的海洋改成 1)
        while q:
            i, j, d = q.popleft()
            ans = max(ans, d)
            for di, dj in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                ni, nj = i + di, j + dj
                if 0 <= ni < n and 0 <= nj < n and grid[ni][nj] == 0:
                    grid[ni][nj] = 1                  # 入队即染色,避免重复入队
                    q.append((ni, nj, d + 1))
        return ans

复杂度:时间 O(n²)、空间 O(n²)

几个工程细节:

第一,全陆地 / 全海洋判退。题目明确要求这两种情况返回 −1。len(q) == n * n 即全陆地(队列装满),not q 即全海洋(队列空)。这两种判断在循环开始前做一次即可。

第二,grid 自己当 visited。把走过的海洋格子改成 1,借助"陆地 = 障碍"语义跟入队条件统一。如果题目要求不能修改输入,开一个独立的 visited 数组即可。

第三,距离 0 也要参与 max——所有陆地起点距离 0,被 ans = max(ans, d) 处理一遍,但 ans = 0 不会被 0 更新(max 不变)。真正起作用的是后续扩张出来的海洋格子。

第四,ans 初值是 0 还是 −1?这道题如果有海洋格子能扩张到,最终 ans 一定 ≥ 1;如果到不了(不会发生,因为已经判退了全陆地 / 全海洋),ans 保留为 0。初值 0 是安全的,对应"刚开始还没找到任何海洋"的状态。


5. 边界、易错与复杂度

把三道题里常踩的坑合并讲。

入队即染色(不是出队时染色)

最常见的退化是"出队时才标 visited"——这会让同一个格子被多次入队(最坏 4 次,每个邻居一次),队列长度爆到 O(4mn),复杂度从 O(mn) 退化到 O(mn) 的 4 倍常数 + 内存浪费。极端情况会 MLE。

正确写法:q.append(...) 之前就把这一格的距离 / 状态写好。这样它再次被相邻格子尝试入队时,会被 dist == -1grid[ni][nj] == 1 拦住。

起点距离记 0,不是 1

把起点距离打成 1 是新手常见 bug。dist[起点] = 0 是约定俗成的——起点到自己的距离当然是 0。BFS 扩张时 dist[邻居] = dist[当前] + 1,从 0 开始累加才是层数。

LC 994 题面说"经过 1 分钟后腐烂",导致有人把初始腐烂橘子记成"分钟 1"。其实"分钟 0 时所有初始腐烂橘子已经腐烂",所以记 0、最后返回最大的 t,对应"第 t 分钟才完成"。

区分"可走" vs "起点"

LC 994 中起点是 grid == 2,可走是 grid != 0(即 1 或 2)。新鲜橘子和腐烂橘子都算可达点,空格子是障碍。

LC 542 中起点是 grid == 0,扩张目标是 grid == 1。没有障碍。

LC 1162 中起点是 grid == 1(陆地),扩张目标是 grid == 0(海洋)。

写多源 BFS 时把这两件事分开想清楚:谁是起点(一次性入队)谁是可走的(在扩张时被判进队列)

队列元素:用元组还是 (i, j, dist)

两种风格:

  • dist[i][j] 二维数组单独存距离:队列里只放 (i, j)。代码更长但内存常数小。
  • 队列元素打成 (i, j, d):把距离塞进队列元素。代码紧凑,但内存多 33%。

LC 994 风格用了 (i, j, d),LC 542 / 1162 用了 dist 矩阵。两种都对,按个人习惯选。Python 里元组很便宜,差异不大;Java / Go 里 int[]{i, j, d}int[]{i, j} + dist[][] 多一次堆分配但代码短。

Python 用 deque 不用 list

Python 的 list.pop(0)O(n),BFS 用 list 会让整体退化到 O((mn)²)。必须 from collections import deque,用 q.popleft() O(1)

Java 用 ArrayDeque、Go 用切片 + 偏移指针都可以;都不要用 LinkedList 这种链表实现,常数大。

起点不存在的退化

每道题都要单独考虑"没有起点 / 全是起点 / 没有目标"这几种情形:

  • LC 994:没有腐烂橘子 + 有新鲜橘子 → −1;没新鲜橘子 → 0。
  • LC 542:题目保证至少一个 0 → 不会退化。
  • LC 1162:全陆地 / 全海洋 → −1。

写完主逻辑后,把这几种边界各跑一遍,保证返回的不是 NaN / 抛异常。

复杂度速查

时间空间
LC 994 腐烂的橘子O(mn)O(mn) 队列
LC 542 01 矩阵O(mn)O(mn) dist + 队列
LC 1162 离海洋最远的陆地O(n²)O(n²) 队列

每道题都是"每个格子至多入队一次、出队一次",所以渐进时间和空间都被网格大小卡死。


Pro 会员

解锁完整北美 OA 题库

165 家公司 / 1000+ 道真题 · Python · Java · C++ 三语题解 · 月度更新


7. 小结

多源 BFS = "所有起点一次性入队"。本质等价于建一个虚拟超级源点零费用连到所有真实起点,从虚拟源跑一次单源 BFS。复杂度 O(mn),跟起点数量无关。三件事记牢:

  • 起点距离 0、入队即染色,避免重复入队。
  • is_sourcecan_walk 分开,看清楚谁是起点、谁是障碍、谁是扩张目标。
  • 退化情形单独判:无起点、无目标、全网格都是起点等情况都要给出正确返回值。

下一节会把网格 BFS 推到"带权"场景——每一步代价不再都是 1,需要 0-1 BFS(双端队列)或 Dijkstra。LC 1631 最小努力路径、LC 1293 网格中的最短路径都属于这类。


对应灵神题单:网格图 / 多源 BFS / 网格上的最短路

进一步阅读:灵茶山艾府《网格图题单》多源 BFS 小节;LeetCode BFS 标签 高频网格题

On this page