多源BFS
定义
多源BFS(多源广度优先搜索)是一种图遍历算法,它是标准BFS(广度优先搜索)的扩展,主要用于解决具有多个起始节点的最短路径问题。在多源BFS中,不是从单一源点开始搜索整个图,而是同时从多个源点出发,寻找这些源点到图中所有其他节点的最短路径。这种方法特别适用于边权都为1的情况,如在网格图中计算点到点的最短曼哈顿距离或欧几里得距离。
多源BFS通过初始化一个队列,将所有源节点放入队列中开始。算法执行标准的BFS过程,但每次从队列中取出一个节点进行扩展时,会检查这个节点是否已经被访问过,以避免重复处理。每个节点的距离是从其最近的源节点测量的,并且维护一个数据结构(如二维数组或字典)来存储每个节点的最短距离。
运用情况
- 网格图中的最短距离:例如,在一个由0和1组成的矩阵中,0表示可以通过的路径,1表示障碍,多源BFS可以用来找出每个位置到最近的0的距离。
- 社交网络中的影响力传播:在社交网络中,如果要计算一个人发布的信息经过多个人传播后能影响到的最远节点,可以将初始发布者作为多个源点进行多源BFS。
- 游戏地图探索:在某些游戏中,玩家可能从多个入口进入迷宫或地图,多源BFS可以用来快速找到各个入口到所有可到达点的最短路径。
注意事项
- 标记已访问:确保每个节点只被访问一次,避免循环和重复计算。
- 初始化源节点的距离:源节点到自身的距离应初始化为0,非源节点的距离可以初始化为无穷大或一个表示未访问的特殊值。
- 使用合适的数据结构:为了高效地管理待访问节点和已访问节点的信息,选择适当的数据结构(如队列、集合、字典等)至关重要。
- 剪枝:在某些情况下,可以通过剪枝策略提前终止不必要的搜索路径,以优化性能。
解题思路
- 初始化:将所有源节点加入队列,并初始化所有节点的距离(源节点为0,其余为无穷大或特殊值)。
- 遍历:执行BFS循环,直到队列为空。每次循环从队列头部取出一个节点,检查其邻居节点。
- 更新距离:对于当前节点的每个未访问邻居,计算从源点通过当前节点到达该邻居的距离,如果这个距离比已知的最短距离更短,则更新该邻居的距离,并将其加入队列。
- 终止条件:当队列为空时,所有可达节点的最短距离都已被计算。
AcWing 173. 矩阵距离
题目描述
173. 矩阵距离 - AcWing题库
运行代码
#include <iostream>
#include <cstring>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 1010;int n, m;
char g[N][N];
PII q[N * N];
int dist[N][N];void bfs()
{int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};int hh = 0, tt = -1;for(int i = 1; i <= n; i ++ )for(int j = 1; j <= m; j ++ )if(g[i][j] == '1'){dist[i][j] = 0;q[++ tt] = {i, j};}while(hh <= tt){PII t = q[hh ++];for(int i =0; i < 4; i ++ ){int a = t.x + dx[i], b = t.y + dy[i];if(a <= 0 || a > n || b <= 0 || b > m) continue;if(dist[a][b] != -1) continue;dist[a][b] = dist[t.x][t.y] + 1;q[++ tt] = {a, b};}}
}int main()
{memset(dist, -1, sizeof dist);cin >> n >> m;for(int i = 0; i < n; i ++ ) cin >> g[i + 1] + 1;bfs();for(int i = 1; i <= n; i ++ ){for(int j = 1; j <= m; j ++ ) cout << dist[i][j] << ' ';cout << endl;}return 0;
}
代码思路
- 数据结构定义与常量设置:
- 使用
PII
(pair<int, int>)类型来表示二维网格中的坐标。 dx[]
和dy[]
数组分别存储了四个基本方向的横纵坐标增量,用于遍历邻居节点。N
定义了网格的最大尺寸,这里设为1010。
- 使用
- 变量初始化:
- 读取网格的行数
n
和列数m
。 - 通过
cin
读取二维字符矩阵g
,'1'代表源点,假设其他位置默认为'0'(虽然代码中未直接处理'0'的情况,但逻辑上是这样理解的)。 - 使用
memset
将距离矩阵dist
初始化为-1,表示尚未访问。
- 读取网格的行数
- 广度优先搜索(BFS)实现:
- 遍历矩阵,将所有值为'1'的点作为起点,其距离设为0,并将它们的坐标存入队列
q
。 - 开始BFS过程:从队列中取出一个点,检查它的四个邻居(上、下、左、右),若邻居坐标在矩阵范围内且尚未访问过(
dist[a][b] == -1
),则更新邻居的距离为当前点的距离加1,并将邻居加入队列。这一步确保了从所有源点出发,逐步扩展到整个网格,同时计算每个点到最近源点的距离。
- 遍历矩阵,将所有值为'1'的点作为起点,其距离设为0,并将它们的坐标存入队列
- 结果输出:遍历二维数组
dist
,按照网格的行列顺序输出每个位置的最短距离,每个数字后面跟一个空格,每行结束后换行打印。
改进思路
-
代码注释和文档:虽然代码逻辑相对清晰,但增加更多的注释说明每个主要步骤的作用、边界条件处理的原因以及算法的核心思想,可以提高代码的可读性和可维护性。
-
异常处理和输入验证:在实际应用中,增加对输入数据的校验是非常重要的。比如,可以检查
n
和m
的范围是否合理,以及输入矩阵是否符合预期格式,以避免运行时错误。 -
空间优化:虽然本代码的空间复杂度已经是线性的,但考虑到在极端情况下(如矩阵几乎全为'1')队列
q
可能会占用大量内存,可以考虑在BFS过程中直接从当前层节点扩展到下一层,而不是将所有节点都存入队列,这样可以减少队列的最大容量需求。 -
并行化处理:如果面对的是非常大的矩阵,可以考虑使用多线程或多进程并行执行BFS,每个线程负责处理一部分源点或区域,以加速计算过程。但这需要引入线程同步机制来避免数据竞争问题。
-
使用STL容器:虽然数组在性能上通常有优势,但考虑到代码的灵活性和易读性,可以考虑使用STL容器如
vector<vector<int>>
来代替二维数组dist
和g
,特别是当矩阵大小不固定或需要动态调整时。 -
常量表达式和类型别名:可以进一步利用C++特性,比如将方向数组定义为
constexpr
以提高效率,或者使用using Grid = vector<vector<char>>;
来定义一个类型别名,使得代码更加现代和清晰。 -
宏替换为内联函数或常量:尽管宏定义在本代码中简化了访问pair成员的操作,但使用内联函数或const变量可以提供更好的类型安全性和调试信息。
改进代码
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
#include <limits>
using namespace std;// 方向向量,表示上下左右四个方向
constexpr array<pair<int, int>, 4> directions = {{{-1, 0}, {0, 1}, {1, 0}, {0, -1}}};// 使用vector替代数组以支持动态大小和更现代的C++实践
using Grid = vector<vector<char>>;
using pii = pair<int, int>; // 简化pair<int, int>的写法int n, m;
Grid g;
vector<vector<int>> dist;
queue<pii> q;// BFS函数,计算每个点到最近'1'的距离
void bfs() {while (!q.empty()) {auto [cx, cy] = q.front(); q.pop();for (const auto& [dx, dy] : directions) {int nx = cx + dx, ny = cy + dy;if (nx >= 0 && nx < n && ny >= 0 && ny < m && g[nx][ny] != '1' && dist[nx][ny] == -1) {dist[nx][ny] = dist[cx][cy] + 1;q.push({nx, ny});}}}
}int main() {cin >> n >> m;g.resize(n, vector<char>(m));dist.assign(n, vector<int>(m, -1)); // 初始化距离矩阵为-1// 读取矩阵for (int i = 0; i < n; ++i) {for (int j = 0; j < m; ++j) {cin >> g[i][j];// 将所有的'1'点加入队列,并初始化其距离为0if (g[i][j] == '1') {dist[i][j] = 0;q.push({i, j});}}}bfs(); // 执行BFS// 输出结果for (const auto& row : dist) {for (int d : row) {cout << (d != -1 ? d : 0) << ' '; // 如果是-1,表示不可达,输出0}cout << endl;}return 0;
}