内容为武汉大学国家网络安全学院2022级大一第三学期“996”实训课程中所做的笔记,仅供个人复习使用,如有侵权请联系本人,将与15个工作日内将博客设置为仅粉丝可见。
目录
广度优先搜索
广度优先搜索,又称宽度优先搜索,简称 BFS,我们以后都会用 BFS 来表示广度优先搜索。与深度优先搜索不同的是,广度优先搜索会先将与起始点距离较近的点搜索完毕,再继续搜索较远的点,而深搜却是沿着一个分支搜到最后。
BFS 从起点开始,优先搜索离起点最近的点,然后由这个最近的点扩展其他稍近的点,这样一层一层的扩展,就像水波扩散一样。
对上图进行深搜按照顶点访问顺序会得到序列:A−B−E−F−C−D−G
对上图进行广搜按照顶点访问顺序会得到序列:A−B−C−D−E−F−G
广度优先搜索的层次关系是很明显的,上面的图的分层次关系如下:
- 第一层的点为 A
- 第二层的点为 B,C,D
- 第三层的点为 E,F,G
BFS 需要借助队列来实现:
-
初始的时候把起始点放到队列中,并标记起点访问。
-
如果队列不为空,从队列中取出一个元素 x,否则算法结束。
-
访问和 x 相连的所有点 v,如果 v 没有被访问,把 v 入队,并标记已经访问。
-
重复执行步骤 2。
最后写出来的代码框架如下:
void bfs(起始点) {
将起始点放入队列中;
标记起点访问;
while (如果队列不为空) {
访问队列中队首元素x;
删除队首元素;
for (x 所有相邻点) {
if (该点未被访问过且合法) {
标记该点访问;
将该点加入队列末尾;
}
}
}
队列为空,广搜结束;
}
注意这里标记一个点是否访问必须在放入队列的时候标记,避免一个点在队列中但没被访问的情况下再次被放入队列的可能。
广度优先搜索演示
略
再探迷宫游戏
前面我们已经接触过了迷宫游戏,并且学会了如何使用 DFS 来解决迷宫最短路问题。用 DFS 求解迷宫最短路有一个很大的缺点,需要枚举所有可能的路径,读入的地图一旦很大,可能的搜索方案数量会非常多,用 DFS 搜索显然效率会很低。
我们可以借助 BFS 来求解迷宫游戏。由于 BFS 是分层搜索,因此,第一次搜索到终点的时候,当前搜索的层数就是最短路径的长度。
迷宫最短路问题参考程序:
#include <stdio.h>
int n, m, l, r;
char maze[110][110];
int vis[110][110];
int dir[4][2] = { { -1, 0}, {0, -1}, {1, 0}, {0, 1}};
int in(int x, int y) {
return 0 <= x && x < n && 0 <= y && y < m;
}
struct node {
int x, y, d;
} q[10010];
int bfs(int sx, int sy) {
l = 0, r = 0;
struct node t = {sx, sy, 0};
q[r++] = t;
vis[sx][sy] = 1;
while (l < r) {
struct node now = q[l++];
if (maze[now.x][now.y] == 'T') {
return now.d;
}
for (int i = 0; i < 4; i++) {
int tx = now.x + dir[i][0];
int ty = now.y + dir[i][1];
if (in(tx, ty) && maze[tx][ty] != '*' && !vis[tx][ty]) {
vis[tx][ty] = 1;
struct node t = {tx, ty, now.d + 1};
q[r++] = t;
}
}
}
return -1;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++) {
scanf("%s", maze[i]);
}
int x, y;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (maze[i][j] == 'S') {
x = i;
y = j;
}
}
}
printf("%d\n", bfs(x, y));
return 0;
}
图上 BFS
在图上,我们也可以进行 BFS,也可以解决图上 DFS 能解决的问题,比如连通块。
除此以外,根据 BFS 的性质,第一次到一个点的时候记下来的步数一定是到从起点到这个点的最短路径的长度,所以我们可以用 BFS 在无权图上求从起点到每个点的最短路。
无权图最短路
那我们来具体研究一下无权图最短路。
基本思想不难,就是从起点开始进行 BFS,我们需要研究的是怎么记录起点到每个点的最短路的长度。
如果枚举每一个点作为终点,每次只求到这个终点的路,那每次搜索中间经过的点的信息就被浪费了,而实际上如果我们不指定终点,当 BFS 的队列最终为空时,它是走到了起点能到的每一个点,并且在走到每一个点的时候都是走的最短路。
所以我们从起点开始只进行一次搜索,不指定终点,队列为空时就结束。
至于中间的结果,可以用一个数组来存储,设数组为 dis ,那刚开始可以全清成 −1,表示这个点没有被到过,起点的 dis 置为 0 ,开始搜索,每次要到一个点的时候更新好相应的 dis 。
此时这个 dis 数组也可以起到原来表示一个点是否访问过的 vis 数组的作用,dis[u] 是 −1−1 就表示 u 没访问过,否则就是访问过。
对于这样的搜索过程,使用邻接表更为方便。
这样搜索求解时间复杂度为 O(n+m) 。
如果需要求任意两点之间的最短路,那就枚举每一个点为起点进行 BFS,时间复杂度为 O(n×(n+m)) 。
无权图最短路求解参考程序:
#include <stdio.h>
#include <string.h>
struct edge {
int v, next;
} e[1000];
int p[100], eid;
void insert(int x, int y) {
e[eid].v = y;
e[eid].next = p[x];
p[x] = eid++;
}
void insert2(int x, int y) {
insert(x, y);
insert(y, x);
}
int n, m, q[1010], l, r;
int dis[105];
void bfs(int s) {
memset(dis, -1, sizeof(dis));
dis[s] = 0;
l = r = 0;
q[r++] = s;
while (l < r) {
int now = q[l++];
for (int i = p[now]; ~i; i = e[i].next) {
int v = e[i].v;
if (dis[v] == -1) {
dis[v] = dis[now] + 1;
q[r++] = v;
}
}
}
}
int main() {
memset(p, -1, sizeof(p));
scanf("%d%d", &n, &m);
for (int i = 0; i < m; i++) {
int u, v;
scanf("%d%d", &u, &v);
insert2(u, v);
}
bfs(1);
for (int i = 1; i <= n; i++) {
printf("%d ", dis[i]);
}
return 0;
}
多起点的 BFS
在普通的广度优先搜索问题中,为了得到从初始状态到达目标状态的最小操作数,则将初始状态放入队列中。离初始状态由近及远地不断扩展出新的状态,直到搜索到目的状态,或队列为空(无法搜索到目标状态),得到结果。
在一些问题中,希望找到离 n 个初始状态距离最小的操作数。在实现这样的问题,主要有两种思路,一是我们可以进行 n 次广度优先搜索,并不断更新表示步数的数组。
而另一种是我们可以将多个初始状态放入队列中,这些状态在队列中扩展得到的结果仍是一个步数不下降的序列,所以当第一次搜索到目标状态时,仍旧是最小的操作数。