多源 BFS

在标准的单源广度优先搜索中,从网格地图中的某一个确定起点出发,逐步按层向四周邻近格子扩张,用来求解网格图/棋盘中的最少移动步数(最短距离)问题

然而,在许多实际题目中,起点的数量往往不止一个。例如:

  • 多个火源同时蔓延,求整栋大楼被烧毁的时间。
  • 多个基地同时发射信号,求网格地图上各个区域最早接收到信号的时间。
  • 求网格图上每个 \(0\) 格子到离它最近\(1\) 格子的距离。

类似这类拥有多个起始位置,且关注每个位置到“最近”起始位置的步数/距离的问题,即为多源 BFS 问题。

为什么不能对每个起点分别做单源 BFS?

假设网格共有 \(R\)\(C\) 列,总格数为 \(N = R \times C\),其中起点共有 \(k\) 个。

如果对每个起点分别跑一次单源 BFS,每次单源 BFS 遍历网格的时间复杂度为 \(O(N)\),总时间复杂度为 \(O(k \cdot N)\)。当起点数量较多,比如 \(k \approx N\) 时,时间复杂度会暴涨至 \(O(N^2) = O((R \times C)^2)\)。在 \(1000 \times 1000\) 的网格中,计算次数高达 \(10^{12}\)

假设网格中存在起点集合 \(S = \{s_1, s_2, \dots, s_k\}\),可以想象存在一个虚构的“超级起点” \(S_0\)

  1. 想象在第 \(0\) 步时,超级起点 \(S_0\) 可以不耗费任何步数(0 步)瞬间到达所有的真实起点 \(s_1, s_2, \dots, s_k\)
  2. 问题便等价转换为了:求从超级起点 \(S_0\) 出发,到达网格中任意格子的最少步数

image

因为从 \(S_0\) 到各个真实起点的步数均为 \(0\),在 BFS 初始化时:

  1. 先将 \(S_0\) 想象为第一层。
  2. 展开 \(S_0\) 后,第一轮扩展会将 \(s_1, s_2, \dots, s_k\) 全部放入队列,且起点步数统一定义为 \(0\)

在实际编写代码时,无需构造复杂的虚构逻辑,只需在算法启动前,将所有起点一次性全部放入 BFS 队列中,作为第 \(0\) 层(初始层)位置即可。

例题:P1332 血色先锋队

在一个 \(n \times m \ (1 \le n,m \le 500)\) 的网格矩阵中,分布着 \(a\) 个感染源和 \(b \ (1 \le a,b \le 10^5)\) 个领主。感染源初始感染时间为 \(0\),从第 \(0\) 小时开始,已被感染过的格子每过 \(1\) 小时会向上下左右相邻的 \(4\) 个格子扩散瘟疫。要求计算出给定的 \(b\) 个领主分别被感染的最早时间,并按输入的顺序输出。

参考代码
#include <iostream>
#include <queue>
using namespace std;
using pi = pair<int, int>;
const int N = 505;
// 定义上下左右四个方向的偏移量
const int X[] = {-1, 1, 0, 0};
const int Y[] = {0, 0, -1, 1};
int d[N][N]; // 记录每个坐标被感染的最早时间
int main()
{
    int n, m, a, b; 
    cin >> n >> m >> a >> b;
    // 初始化为 -1,表示尚未被感染
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            d[i][j] = -1;
        }
    }
    queue<pi> q;
    // 读入所有感染源并作为多源起点同时入队
    while (a--) {
        int x, y; cin >> x >> y;
        if (d[x][y] == -1) {
            d[x][y] = 0;
            q.push({x, y});
        }
    }
    // 多源广度优先搜索(BFS)
    while (!q.empty()) {
        pi p = q.front();
        q.pop();
        int x = p.first, y = p.second;
        for (int i = 0; i < 4; i++) {
            int nx = x + X[i], ny = y + Y[i];
            // 检查边界及未访问状态
            if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && d[nx][ny] == -1) {
                d[nx][ny] = d[x][y] + 1; // 距离加 1
                q.push({nx, ny});
            }
        }
    }
    // 依次回答 b 个领主的查询
    while (b--) {
        int x, y; cin >> x >> y;
        cout << d[x][y] << "\n";
    }
    return 0;
}

网格图上共 \(n \times m\) 个节点,BFS 保证每个节点最多入队和出队一次,处理四周邻居节点耗时与总节点数成正比,即 \(O(n \times m)\)。多源起点入队耗时 \(O(a)\),最后的查询阶段回答 \(b\) 个领主的位置耗时 \(O(b)\),总时间复杂度为 \(O(n \times m + a + b)\)

习题:P1256 显示图像

解题思路

如果对每个黑色像素点都进行一次 BFS 来寻找最近的白色像素点,时间复杂度将达到 \(O(N^2 M^2)\),会超时。

可以反过来思考,将所有的白色像素点作为“源点”,同时加入 BFS 队列。根据 BFS 逐层搜索的性质,BFS 的过程就相当于每个起点先扩展 1 层,再扩展 2 层,3 层,以此类推。所以当每个位置 \((x,y)\) 第一次被访问时,就相当于距离它最近的那个起点扩展到了它。

image

每个像素点最多入队一次,出队一次,且每个点仅访问其四个相邻点,因此时间复杂度为 \(O(NM)\)

参考代码
#include <cstdio>
#include <queue>
using namespace std;
const int N = 185;
const int dx[] = {-1, 1, 0, 0};
const int dy[] = {0, 0, -1, 1};
// 结构体存储坐标
struct Point {
    int x, y;
};
char row[N];
int d[N][N];
int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    queue<Point> q;
    // 初始化距离数组为 -1
    for (int i = 1; i <= n; i++) {
        scanf("%s", row);
        for (int j = 1; j <= m; j++) {
            if (row[j - 1] == '1') {
                d[i][j] = 0; // 白色像素点距离为 0
                q.push({i, j}); // 将所有源点入队
            } else {
                d[i][j] = -1; // 黑色像素点标记为未访问
            }
        }
    }
    // 多源 BFS 过程
    while (!q.empty()) {
        Point cur = q.front();
        q.pop();
        for (int i = 0; i < 4; i++) {
            int nx = cur.x + dx[i];
            int ny = cur.y + dy[i];
            // 检查边界及是否访问过
            if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && d[nx][ny] == -1) {
                d[nx][ny] = d[cur.x][cur.y] + 1;
                q.push({nx, ny});
            }
        }
    }
    // 输出结果矩阵
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            printf("%d ", d[i][j]);
        }
        printf("\n");
    }
    return 0;
}
posted @ 2026-08-02 15:37  RonChen  阅读(14)  评论(0)    收藏  举报