知识总结
广度优先搜索(Breadth First Search)
BFS 全称是 Breadth First Search,中文名是宽度优先搜索,也叫广度优先搜索。是图上最基础、最重要的搜索算法之一。
所谓宽度优先。就是每次都尝试访问同一层的节点。 如果同一层都访问完了,再访问下一层。这样做能使 BFS 算法找到的路径是从起点开始的 最短 合法路径。换言之,这条路径所包含的边数最小。在 BFS 结束时,每个节点都是通过从起点到该点的 最短路径 访问的。
DFS vs BFS
小结:DFS 适合于找给定起点到终点的所有解, BFS 适用于找最短路(最优解)。
BFS 的存储实现:队列
思路
- 用一个队列 Q 来记录要处理的节点,然后开一个布尔数组 vis[] 来标记是否已经访问过某个节点。
- 开始的时候,我们将所有节点的 vis 值设为 0,表示没有访问过;然后把起点 s 放入队列 Q 中并将 vis[] 设为 1。
- 之后,我们每次从队列 Q 中取出队首的节点 u,然后把与 u 相邻的所有节点 v 标记为已访问过并放入队列 Q。
- 循环直至当队列 Q 为空,表示 BFS 结束。
伪代码
q.push(初始状态); while(!q.empty()){ status t = q.front(); // 取队首状态 q.pop(); // 队首状态出队 for(枚举所有可扩展新状态 v){ if(新状态 v 合法) q.push(v); // 新状态 v 入队 // 维护一些必要信息 } }
在 BFS 的过程中,也可以记录一些额外的信息。例如:
- 用 d 数组记录起点到某个节点的最短距离(要经过的最少边数),可以方便地得到起点到一个节点的距离。
- 用 f 数组记录是从哪个节点走到当前节点的,可以方便地还原出从起点到一个点的最短路径。
复杂度
时间复杂度
总共有 n 个顶点,m 条边要访问,因此时间复杂度是 O(n + m) 。
空间复杂度
需要标记数组 vis[] 和一个队列,大小为结点数,因此空间复杂度是 O(n)。
应用
- 判断地图中各点间的连通性。
- 在地图中搜索从给定起点到其他所有点的最短路径。
- 在 O(n + m) 时间内求出所有连通块。(从每个没有被访问过的节点开始做 BFS,每次 BFS 会走完一个连通块)。
最短路径类-经典例题
奇怪的电梯
思路
从起点楼层开始 BFS,记录每层楼下一步可达楼层及所需按键次数。
代码-1:DFS(求最短路需剪枝优化)
#include <bits/stdc++.h> using namespace std; int n, a, b, k[205], c[205]; int e[2] = {-1, 1}; void dfs(int d, int x){ // 当前到达第 d 层,按键次数 x 次 c[d] = x; // 数组中记录 for(int i = 0; i < 2; i++){ int t = d + e[i] * k[d]; if(t >= 1 && t <= n && c[t] > x + 1){ // 仅在可优化时深搜下一步 dfs(t, x + 1); } } } int main(){ cin >> n >> a >> b; for(int i = 1; i <= n; i++){ cin >> k[i]; } memset(c, 0x7f, sizeof(c)); dfs(a, 0); cout << (c[b] != 0x7f7f7f7f ? c[b] : -1); return 0; }
代码-2:BFS 求线性最短路
#include <bits/stdc++.h> using namespace std; int n, a, b, k[205], c[205]; queue<int> q; void bfs(){ memset(c, -1, sizeof(c)); // 初始化每层楼状态不可达 q.push(a); // 起点入队 c[a] = 0; // 初始化起点 0 次按电梯 while(!q.empty()){ int u = q.front(); // 取队首 q.pop(); for(int i = -1; i <= 1; i++){ if(!i) continue; int v = u + i * k[u]; // 从 u 出发,下次可达 u-k[u] 或 u+k[u] if(v >= 1 && v <= n && c[v] == -1){ // 边界合法且未访问过 c[v] = c[u] + 1; // u -> v,v 按键次数是 c[u] + 1 q.push(v); // 将楼层 v 入队 } } } cout << c[b]; // 输出 b 需要的按键次数 } int main(){ cin >> n >> a >> b; for(int i = 1; i <= n; i++){ cin >> k[i]; } bfs(); return 0; }
The Chivalrous Cow B
思路
求给定起点到终点的最短路,用 BFS 即可,输入地图时存入搜索起点坐标和终点坐标。
代码
#include <bits/stdc++.h> using namespace std; int n, m, sx, sy, fx, fy, d[155][155]; char mp[155][155]; int b[8][2] = {{-2, -1}, {-2, 1}, {-1, -2}, {-1, 2}, {1, -2}, {1, 2}, {2, -1}, {2, 1}}; void bfs(int x, int y){ queue<int> qx, qy; qx.push(x), qy.push(y); while(!qx.empty()){ int xx = qx.front(), yy = qy.front(); qx.pop(), qy.pop(); for(int i = 0; i < 8; i++){ int x1 = xx + b[i][0], y1 = yy + b[i][1]; if(x1 >= 1 && x1 <= n && y1 >= 1 && y1 <= m && mp[x1][y1] != '*' && d[x1][y1] == -1){ d[x1][y1] = d[xx][yy] + 1; qx.push(x1), qy.push(y1); } } } } int main(){ scanf("%d %d\n", &m, &n); for(int i = 1; i <= n; i++){ scanf("%s", mp[i] + 1); // 从 mp[i][1] 开始存 for(int j = 1; j <= m; j++){ if(mp[i][j] == 'K'){ sx = i, sy = j; } if(mp[i][j] == 'H'){ fx = i, fy = j; } } } memset(d, -1, sizeof(d)); d[sx][sy] = 0; // 初始化起点 bfs(sx, sy); printf("%d", d[fx][fy]); return 0; }
马的遍历
思路
用队列实现 bfs() 扩展下一步的过程,要注意马的走法,马走日,且八个方向都可扩展。
代码
#include <bits/stdc++.h> using namespace std; int n, m, x, y; int dis[4005][4005]; // dis[x][y] 存起点到 (x, y)最短路径的距离 int b[8][2] = {{1, 2}, {1, -2}, {-1, 2}, {-1, -2}, {2, 1}, {2, -1}, {-2, 1}, {-2, -1}}; // 8 个扩展方向 queue<int> qx, qy; void bfs(){ memset(dis, -1, sizeof(dis)); // 每个字节都赋值 (11111111) ,即数组初始化为 -1 qx.push(x); qy.push(y); // 初始状态入队 dis[x][y] = 0; // 初始距离为 0 while(!qx.empty()){ int tx = qx.front(), ty = qy.front(); // 取坐标值 qx.pop(); qy.pop(); // 访问过状态出队 for(int i = 0; i < 8; i++){ // 扩展由 tx, ty 出发的下一步 int x1 = tx + b[i][0], y1 = ty + b[i][1]; if(x1 >= 1 && x1 <= n && y1 >= 1 && y1 <= m && dis[x1][y1] == -1){ // 未越界且未访问过的下一步 qx.push(x1); qy.push(y1); // 新状态入队 dis[x1][y1] = dis[tx][ty] + 1; // 更新下一步距离 } } } } int main(){ cin >> n >> m >> x >> y; bfs(); for(int i = 1; i <= n; i++){ for(int j = 1; j <= m; j++){ printf("%-5d", dis[i][j]); // 左对齐 5 个场宽 } printf("\n"); } return 0; }
迷宫问题(打印最短路)
思路
在访问到每个结点时记录它的父节点信息,遍历路径时先从终点开始倒推将经过结点入栈,最后从栈顶起点开始输出。
代码
#include <bits/stdc++.h> #define maxn 10 using namespace std; int a[maxn][maxn], d[maxn][maxn], f[maxn][maxn][2]; // f[i][j] 记录父节点 int b[4][2] = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}}; // 上左下右 void bfs(){ d[0][0] = 0; // 起点距离初始化 queue<int> qx, qy; qx.push(0), qy.push(0); while(!qx.empty()){ int xx = qx.front(), yy = qy.front(); qx.pop(), qy.pop(); for(int i = 0; i < 4; i++){ int x1 = xx + b[i][0], y1 = yy + b[i][1]; if(x1 >= 0 && x1 <= 4 && y1 >= 0 && y1 <= 4 && !a[x1][y1] && d[x1][y1] == -1){ d[x1][y1] = d[xx][yy] + 1; // (xx, yy) -> (x1, y1) f[x1][y1][0] = xx, f[x1][y1][1] = yy; // 记录 (x1, y1) 的父节点 (xx, yy) qx.push(x1), qy.push(y1); } } } } void prt(){ stack<int> sx, sy; int x = 4, y = 4; while(x || y){ // 没到起点 (0, 0) sx.push(x), sy.push(y); int tx = sx.top(), ty = sy.top(); x = f[tx][ty][0], y = f[tx][ty][1]; } sx.push(0), sy.push(0); // 起点放栈顶 while(!sx.empty()){ printf("(%d, %d)\n", sx.top(), sy.top()); sx.pop(), sy.pop(); } } int main(){ for(int i = 0; i < 5; i++){ for(int j = 0; j < 5; j++){ scanf("%d", &a[i][j]); } } memset(d, -1, sizeof(d)); // 初始化距离数组 -1 表示未访问 bfs(); prt(); return 0; }
连通性求解类-经典例题
入门
思路
求给定起点所在连通块的元素个数,从给定点开始 bfs 即可。
代码
#include <bits/stdc++.h> using namespace std; int h, w, s, sx, sy; int b[4][2] = {{0, -1}, {-1, 0}, {0, 1}, {1, 0}}; char a[55][55]; int bfs(int x, int y){ queue<int> qx, qy; qx.push(x), qy.push(y); s++; // 初始黑色瓷砖算上 while(!qx.empty()){ int x1 = qx.front(), y1 = qy.front(); qx.pop(), qy.pop(); for(int i = 0; i < 4; i++){ int x2 = x1 + b[i][0], y2 = y1 + b[i][1]; if(x2 >= 1 && x2 <= h && y2 >= 1 && y2 <= w && a[x2][y2] == '.'){ qx.push(x2), qy.push(y2); s++; a[x2][y2] = ' '; } } } return s; } int main(){ scanf("%d %d", &w, &h); for(int i = 1; i <= h; i++){ for(int j = 1; j <= w; j++){ cin >> a[i][j]; if(a[i][j] == '@'){ sx = i, sy = j; } } } printf("%d", bfs(sx, sy)); return 0; }
01迷宫
思路
求解给定坐标所在连通块的元素个数,如果询问坐标点还未搜索过所在连通块,以该点作为起点进行 bfs,同步计数连通块个数,否则直接输出之前搜索过程中标记的连通块及其个数。
代码
#include <bits/stdc++.h> using namespace std; // f[][] 记录所在连通块, ans[] 记录连通块元素个数 int n, m, x, y, s, f[1005][1005], ans[1000005]; char a[1005][1005]; int d[4][2] = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}}; void bfs(int x, int y){ if(f[x][y]){ printf("%d\n", ans[f[x][y]]); return; } queue<int> qx, qy; qx.push(x), qy.push(y); s++; // s 记录是第几个连通块 f[x][y] = s; // 标记起点连通块编号 int cnt = 1; // 计数该连通块元素个数 while(!qx.empty()){ int x0 = qx.front(), y0 = qy.front(); qx.pop(), qy.pop(); for(int i = 0; i < 4; i++){ int x1 = x0 + d[i][0], y1 = y0 + d[i][1]; if(abs(a[x1][y1] - a[x0][y0]) == 1 && !f[x1][y1]){ qx.push(x1), qy.push(y1); f[x1][y1] = s; // 标记该点的连通块编号 cnt++; // 该连通块计数加 1 } } } ans[s] = cnt; printf("%d\n", cnt); } int main(){ scanf("%d %d", &n, &m); for(int i = 1; i <= n; i++){ scanf("%s", a[i] + 1); // 从 a[i][1] 开始存 } while(m--){ scanf("%d %d", &x, &y); bfs(x, y); } return 0; }
填涂颜色
思路
正面寻找闭合圈内的 0 可能不太好找,可以从反面,将与边界相连的 0 标记为 “非闭合圈” 0 的状态,比如用 -1 标记。这样就可以从 (0, 0) 开始搜索与边界相连的所有“非闭合圈” 0,进行标记染色,最后矩阵中剩下未染色的 0 就是闭合圈中的 0。输出时进行分支判断即可。
代码
#include <bits/stdc++.h> #define maxn 105 using namespace std; int n, a[maxn][maxn]; int d[4][2] = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; queue<int> qx, qy; void bfs(int x, int y){ a[x][y] = -1; // 标记起点 qx.push(x), qy.push(y); while(!qx.empty()){ int x1 = qx.front(), y1 = qy.front(); qx.pop(), qy.pop(); for(int i = 0; i < 4; i++){ int x2 = x1 + d[i][0], y2 = y1 + d[i][1]; if(x2 >= 0 && x2 <= n + 1 && y2 >= 0 && y2 <= n + 1 && !a[x2][y2]){ qx.push(x2), qy.push(y2); a[x2][y2] = -1; // 特殊标记 } } } } int main(){ cin >> n; for(int i = 1; i <= n; i++){ for(int j = 1; j <= n; j++){ cin >> a[i][j]; } } bfs(0, 0); // (0, 0) 开始连通的 0 不属于闭合圈,将这块找到标记为 -1 for(int i = 1; i <= n; i++){ for(int j = 1; j <= n; j++){ if(a[i][j] == 0) cout << 2 << " "; else if(a[i][j] == -1) cout << 0 << " "; else cout << a[i][j] << " "; } cout << "\n"; } return 0; }
拓展习题
Meteor Shower S
思路
首先预处理每个位置被流星烧焦的最早时间,然后从 (0, 0) 开始 bfs 每个点的到达时间,如果到达时间小于该点被烧焦时间,即为可达点,更新到达时间,找出所有可到达的点。
最后遍历每个点,在左右可到达的点中找到最早的一个安全点(不会被流星烧焦的点),记录最早时间,如果找不到就是不存在安全位置。
代码
#include <bits/stdc++.h> using namespace std; int m, x, y, t, d[310][310], a[310][310], ans = INT_MAX; int b[5][2] = {{0, 0}, {-1, 0}, {0, -1}, {1, 0}, {0, 1}}; queue<int> qx, qy; int main(){ memset(d, 0x7f, sizeof(d)); scanf("%d ", &m); while(m--){ scanf("%d %d %d", &x, &y, &t); for(int i = 0; i <= 4; i++){ int x1 = x + b[i][0], y1 = y + b[i][1]; if(x1 >= 0 && y1 >= 0) d[x1][y1] = min(d[x1][y1], t); } } memset(a, -1, sizeof(a)); a[0][0] = 0; qx.push(0), qy.push(0); while(!qx.empty()){ int xx = qx.front(), yy = qy.front(); qx.pop(), qy.pop(); for(int i = 1; i <= 4; i++){ int x1 = xx + b[i][0], y1 = yy + b[i][1]; if(x1 >= 0 && y1 >= 0 && a[x1][y1] == -1 && a[xx][yy] + 1 < d[x1][y1]){ a[x1][y1] = a[xx][yy] + 1; qx.push(x1), qy.push(y1); } } } for(int i = 0; i < 305; i++){ for(int j = 0; j < 305; j++){ if(a[i][j] != -1 && d[i][j] == 0x7f7f7f7f){ // 可到达的不会有流星的点 ans = min(ans, a[i][j]); } } } if(ans == INT_MAX){ printf("-1"); } else{ printf("%d", ans); } return 0; }
最后的迷宫
思路
本题的二维行列坐标需要降维才能存储下。
代码
#include <bits/stdc++.h> using namespace std; int n, m, ans, dis[16500]; // dis[i]:对降维后的坐标 i, 哈利需要 dis[i] 步能到该点 int hx, hy, jx, jy; char a[16500], b[16500]; // b[] 用来标记哪些位置能看到奖杯 const int d[8][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}}; int c(int x, int y){ // 把二维矩阵坐标转成一维 (i, j) -> (i-1) * m + j return (x-1) * m + y; } void bfs(){ memset(dis, -1, sizeof(dis)); // 每次搜索前重新初始化距离数组 queue<int> qx, qy; qx.push(hx), qy.push(hy); // 哈利坐标入队 dis[c(hx, hy)] = 0; // 起始点距离为 0 while(!qx.empty()){ int x = qx.front(), y = qy.front(); // 取队首坐标 qx.pop(), qy.pop(); // 弹出队首坐标 if(b[c(x, y)] == 'Y') { // 若该处能看到奖杯 ans = dis[c(x, y)]; // 搜索到的最短路就是答案 return; } for(int i = 0; i < 4; i++){ // 遍历 (x, y) 的四个方向 int x1 = x + d[i][0], y1 = y + d[i][1]; if(x1 >= 1 && x1 <= n && y1 >= 1 && y1 <= m // 未越界 && dis[c(x1, y1)] == -1 && b[c(x1, y1)] != 'X'){ // 未去过的空地 dis[c(x1, y1)] = dis[c(x, y)] + 1; // 更新距离 qx.push(x1), qy.push(y1); // 新坐标入队 } } } } int main(){ cin >> n >> m; for(int i = 1; i <= n * m; i++) cin >> a[i]; while(cin >> jx >> jy >> hx >> hy){ if(!hx && !hy && !jx && !jy) break; memcpy(b, a, sizeof(a)); b[c(jx, jy)] = 'Y'; // 奖杯 & 能看到奖杯的位置,都标成 Y // 遍历奖杯周围的八个方向,标记能看到奖杯的位置 for(int i = 0; i < 8; i++){ int x1 = jx + d[i][0], y1 = jy + d[i][1]; // 下个位置 while(x1 >= 1 && x1 <= n && y1 >= 1 && y1 <= m && b[c(x1, y1)] == 'O') { // 该方向望到头 b[c(x1, y1)] = 'Y'; // 可以看到 x1 += d[i][0], y1 += d[i][1]; // 该方向下一步 } } ans = -1; // 搜索前重新初始化答案 bfs(); if(ans != -1) cout << ans << "\n"; else cout << "Poor Harry\n"; } return 0; }
拓展资料
- OI-Wiki:BFS(图论)
- 洛谷题单:【算法1-7】搜索
- POJ-NOI:2.5基本算法之搜索