宽度优先搜索BFS

宽度优先搜索(BFS,Breadth-First Search)也是搜索的手段之一。它与深度优先搜索类似,从某个状态出发探索所有可以到达的状态。

与深度优先搜索的不同之处在于搜索的顺序,宽度优先搜索总是先搜索距离初始状态近的状态。也就是说,它是按照开始状态→只需1次转移就可以到达的所有状态→只需2次转移就可以到达的所有状态→……这样的顺序进行搜索。对于同一个状态,宽度优先搜索只经过一次,因此复杂度为O(状态数×转移的方式)。

《宽度优先搜索BFS》[+]查看原图
状态转移的顺序

深度优先搜索(隐式地)利用了栈进行计算,而宽度优先搜索则利用了队列。搜索时首先将初始状态添加到队列里,此后从队列的最前端不断取出状态,把从该状态可以转移到的状态中尚未访问过的部分加入队列,如此往复,直至队列被取空或找到了问题的解。通过观察这个队列,我们可以就知道所有的状态都是按照距初始状态由近及远的顺序被遍历的。

迷宫的最短路径
给定一个大小为N×M的迷宫。迷宫由通道和墙壁组成,每一步可以向邻接的上下左右四格的通道移动。请求出从起点到终点所需的最小步数。请注意,本题假定从起点一定可以移动到终点。
限制条件

  • N, M ≤ 100

样例

输入

N=10, M=10(迷宫如下图所示。'#''.''S''G'分别表示墙壁、通道、起点和终点) #S######.# ......#..# .#.##.##.# .#........ ##.##.#### ....#....# .#######.# ....#..... .####.###. ....#...G#

输出

22

宽度优先搜索按照距开始状态由近及远的顺序进行搜索,因此可以很容易地用来求最短路径、最少操作之类问题的答案。这个问题中,状态仅仅是目前所在位置的座标,因此可以构造成pair或者编码成int来表达状态。当状态更加复杂时,就需要封装成一个类来表示状态了。转移的方式为四方向移动,状态数与迷宫的大小是相等的,所以复杂度是O(4×N×M)=O(N×M)。

宽度优先搜索中,只要将已经访问过的状态用标记管理起来,就可以很好地做到由近及远的搜索。这个问题中由于要求最短距离,不妨用d[N][M]数组把最短距离保存起来。初始时用充分大的常数INF来初始化它,这样尚未到达的位置就是INF,也就同时起到了标记的作用。

虽然到达终点时就会停止搜索,可如果继续下去直到队列为空的话,就可以计算出到各个位置的最短距离。此外,如果搜索到最后,d依然为INF的话,便可得知这个位置就是无法从起点到达的位置。

在今后的程序中,使用像INF这样充分大的常数的情况还很多。不把INF当作例外,而是直接参与普通运算的情况也很常见。这种情况下,如果INF过大就可能带来溢出的危险。

假设INF=231-1。例如想用d[nx][ny]=min(d[nx][ny], d[x][y]+1)来更新d[nx][ny],就会发生INF+1=-231的情况。这一问题中d[x][y]总不等于INF,所以没有问题。但是为了防止这样的问题,一般会将INF设为放大2~4倍也不会溢出的大小(可参考2.5节Floyd-Warshall算法等)。

因为要向4个不同方向移动,用dx[4]和dy[4]两个数组来表示四个方向向量。这样通过一个循环就可以实现四方向移动的遍历。

const int INF = 100000000; // 使用pair表示状态时,使用typedef会更加方便一些 typedef pair<int, int> P; // 输入 char maze[MAX_N][MAX_M + 1]; // 表示迷宫的字符串的数组 int N, M; int sx, sy; // 起点座标 int gx, gy; // 终点座标 int d[MAX_N][MAX_M]; // 到各个位置的最短距离的数组 // 4个方向移动的向量 int dx[4] = {1, 0, -1, 0}, dy[4] = {0, 1, 0, -1}; // 求从(sx, sy)到(gx, gy)的最短距离 // 如果无法到达,则是INF int bfs() { queue<P> que; // 把所有的位置都初始化为INF for (int i = 0; i < N; i++) for (int j = 0; j < M; j++) d[i][j] = INF; // 将起点加入队列,并把这一地点的距离设置为0 que.push(P(sx, sy)); d[sx][sy] = 0; // 不断循环直到队列的长度为0 while (que.size()) { // 从队列的最前端取出元素 P p = que.front(); que.pop(); // 如果取出的状态已经是终点,则结束搜索 if (p.first == gx && p.second == gy) break; // 四个方向的循环 for (int i = 0; i < 4; i++) { // 移动之后的位置记为(nx, ny) int nx = p.first + dx[i], ny = p.second + dy[i]; // 判断是否可以移动以及是否已经访问过(d[nx][ny]!=INF即为已经访问过) if (0 <= nx && nx < N && 0 <= ny && ny < M && maze[nx][ny] != '#' && d[nx][ny] == INF) { // 可以移动的话,则加入到队列,并且到该位置的距离确定为到p的距离+1 que.push(P(nx, ny)); d[nx][ny] = d[p.first][p.second] + 1; } } } return d[gx][gy]; } void solve() { int res = bfs(); printf("%d\n", res); }

宽度优先搜索与深度优先搜索一样,都会生成所有能够遍历到的状态,因此需要对所有状态进行处理时使用宽度优先搜索也是可以的。但是递归函数可以很简短地编写,而且状态的管理也更简单,所以大多数情况下还是用深度优先搜索实现。反之,在求取最短路时深度优先搜索需要反复经过同样的状态,所以此时还是使用宽度优先搜索为好。

宽度优先搜索会把状态逐个加入队列,因此通常需要与状态数成正比的内存空间。反之,深度优先搜索是与最大的递归深度成正比的。一般与状态数相比,递归的深度并不会太大,所以可以认为深度优先搜索更加节省内存。

此外,也有采用与宽度优先搜索类似的状态转移顺序,并且注重节约内存占用的迭代加深深度优先搜索(IDDFS,Iterative Deepening Depth-First Search)。IDDFS是一种在最开始将深度优先搜索的递归次数限制在1次,在找到解之前不断增加递归深度的方法。这种方法会在4.5节详细介绍。

点赞