零、BFS的两大模型和使用情景
BFS两大模型:
- 最短距离模型 (如基础课“走迷宫”一题):求方格矩阵中,求以某个点为起点,走到目标终点的最短距离
- 最小步数模型 (如基础课“八数码”一题):把矩阵看成一种状态,求从一种状态到另一种状态的最小变换次数
BFS使用情景:
- “求最小”,即BFS第一次搜到目标结果时一定是最小值
- 基于迭代,相比于DFS不会爆栈 (算法题中一般默认栈空间为1M)
一、Flood Fill 概述
Flood Fill (洪水填充法),可在线性时间复杂度内找到某个点所在的连通块。
譬如在一个地形图中,每个小方格有两种高度:“凸起”和“凹进”,为了找到一块相连通的"凹进" (称其为“洼地”),我们可以先找到任意一块“凹进”,开始注水 (水的总量无限,不会流干)。凭生活经验,水肯定会向四周同是“凹进”的地方蔓延,且不会蔓延至更高的“凸起”。当水不再蔓延时,水所有覆盖的地方既是一块“洼地”。这个过程如下图所示:
“水”一次次蔓延的过程可以用BFS或者DFS模拟,BFS是“一圈一圈向外蔓延”,DFS则是“沿着一条路线一直流”。由于DFS在深度较大时可能会爆栈,我们一般用BFS模拟蔓延的过程。
二、池塘计数
本题要求找到所有相连的“W”块,可以使用Flood Fill方法实现。从前往后寻找第一个“W”块,找到后将池塘数量 + 1 +1 +1 ,并对其“注水”,使得所有与其相连的“W”块均被覆盖。重复以上过程,直到所有“W”块已被覆盖。
代码实现:
#include <cstdio>
#include <algorithm>
#include <cstring>
#define x first
#define y second
using namespace std;
typedef pair <int, int> PII;
const int N = 1010, M = N * N;
int n, m;
char g[N][N];
PII q[M];
bool st[N][N];
void bfs(int sx, int sy){ //具体的Flood Fill过程,基于BFS实现
int hh = 0, tt = 0;
q[0] = {sx, sy};
st[sx][sy] = 1;
while (hh <= tt){
PII t = q[hh ++];
for (int i = t.x - 1; i <= t.x + 1; i ++)
for (int j = t.y - 1; j <= t.y + 1; j ++){
if (i == t.x && j == t.y) continue;
if (i < 0 || i >= n || j < 0 || j >= m) continue;
if (g[i][j] == '.' || st[i][j]) continue;
q[++ tt] = {i, j};
st[i][j] = 1;
}
}
}
int main(){
scanf("%d %d", &n, &m);
for (int i = 0; i < n; i ++) scanf("%s", g[i]);
int cnt = 0;
for (int i = 0; i < n; i ++)
for (int j = 0; j < m; j ++)
if (g[i][j] == 'W' && !st[i][j]){ //找到当前未被覆盖的第一个“W”块
bfs(i, j); //对其进行Flood Fill操作
cnt ++; //记得将池塘数+1
}
printf("%d\n", cnt);
return 0;
}
三、城堡问题
本题需要统计房间总数和最大房间的面积,与上题基本相同。注意本题中“墙壁”用二进制方法给出,在判断联通时需要使用位运算。在统计房间面积时,在入队或出队时加一均可,下面给出的代码是在出队时加一。
代码实现:
#include <cstdio>
#include <algorithm>
#include <cstring>
#define x first
#define y second
using namespace std;
typedef pair <int, int> PII;
const int N = 55, M = N * N;
int n, m;
int g[N][N];
PII q[M];
bool st[N][N];
int bfs(int sx, int sy){
int dx[4] = {0, -1, 0, 1}, dy[4] = {-1, 0, 1, 0}; //按照题目顺序,西北东南
int hh = 0, tt = 0;
int area = 0;
q[0] = {sx, sy};
st[sx][sy] = 1;
while (hh <= tt){
PII t = q[hh ++];
area ++; //每次出队,房间面积加一
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 (st[a][b]) continue;
if (g[t.x][t.y] >> i & 1) continue; //提取二进制数的第i位,判断之间是否有墙壁
q[++ tt] = {a, b};
st[a][b] = 1;
}
}
return area;
}
int main(){
scanf("%d %d", &n, &m);
for (int i = 0; i < n; i ++)
for (int j = 0; j < m; j ++)
scanf("%d", &g[i][j]);
int cnt = 0, area = 0;
for (int i = 0; i < n; i ++)
for (int j = 0; j < m; j ++)
if (!st[i][j]){
area = max(area, bfs(i, j));
cnt ++;
}
printf("%d\n%d\n", cnt, area);
return 0;
}
四、山峰和山谷
本题需要统计山峰和山谷的个数,其中山峰指的是周围没有比它还高的一片区域,山谷是周围没有比它还低的一片区域。在Flood Fill过程中,如果周围的方格高度不同,需要判断它比当前区域高还是低,如果更高,当前区域不会是山峰;如果更低,当前区域不会是山谷。
代码实现:
#include <cstdio>
#include <cstring>
#include <algorithm>
#define x first
#define y second
using namespace std;
typedef pair <int, int> PII;
const int N = 1010, M = N * N;
int n;
int h[N][N];
PII q[M];
bool st[N][N];
void bfs(int sx, int sy, bool &has_higher, bool &has_lower){
int hh = 0, tt = 0;
q[0] = {sx, sy};
st[sx][sy] = 1;
while (hh <= tt){
PII t = q[hh ++];
for (int i = t.x - 1; i <= t.x + 1; i ++)
for (int j = t.y - 1; j <= t.y + 1; j ++){
if (i == t.x && j == t.y) continue;
if (i < 0 || i >= n || j < 0 || j >= n) continue;
if (h[i][j] != h[t.x][t.y]){
if (h[i][j] > h[t.x][t.y]) has_higher = 1;
else has_lower = 1;
}
else if (!st[i][j]){
q[++ tt] = {i, j};
st[i][j] = 1;
}
}
}
}
int main(){
scanf("%d", &n);
for (int i = 0; i < n; i ++)
for (int j = 0; j < n; j ++)
scanf("%d", &h[i][j]);
int peak = 0, valley = 0;
for (int i = 0; i < n; i ++)
for (int j = 0; j < n; j ++){
if (!st[i][j]){
bool has_higher = 0, has_lower = 0;
bfs(i, j, has_higher, has_lower);
if (!has_higher) peak ++;
if (!has_lower) valley ++; //不能用else,可能既是山峰又是山谷
}
}
printf("%d %d\n", peak, valley);
return 0;
}
五、迷宫问题
本题在“走迷宫”的基础上要求记录路径。在BFS时,记录每个点是由哪个点走到的,最后逆序输出即可。当然也可反向BFS,将终点和起点互换,记录反向路径后直接正向输出。
代码实现:
#include <cstdio>
#include <cstring>
#include <algorithm>
#define x first
#define y second
using namespace std;
typedef pair <int, int> PII;
const int N = 1010, M = N * N;
int n;
int g[N][N];
PII q[M];
PII pre[N][N];
void bfs(int sx, int sy){
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int hh = 0, tt = 0;
q[0] = {sx, sy};
memset(pre, -1, sizeof pre);
pre[sx][sy] = {0, 0};
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 >= n) continue;
if (g[a][b]) continue;
if (pre[a][b].x != -1) continue;
q[++ tt] = {a, b};
pre[a][b] = t;
}
}
}
int main(){
scanf("%d", &n);
for (int i = 0; i < n; i ++)
for (int j = 0; j < n; j ++)
scanf("%d", &g[i][j]);
bfs(n - 1, n - 1); //反向bfs,起点为(n-1,n-1),终点为(0,0),这样记录的路径就是正向的
PII end(0, 0);
while (1){
printf("%d %d\n", end.x, end.y);
if (end.x == n - 1 && end.y == n - 1) break;
end = pre[end.x][end.y];
}
return 0;
}
六、武士风度的牛
普通的BFS问题,马走日。
代码实现:
#include <iostream>
#include <cstring>
#include <algorithm>
#define x first
#define y second
using namespace std;
typedef pair <int, int> PII;
const int N = 155, M = N * N;
int n, m;
char g[N][N];
PII q[M];
int dist[N][N];
int bfs(){
int dx[] = {-2, -1, 1, 2, 2, 1, -1, -2}; //c++中如果是一维数组且有初始值,可以不用写数组长度
int dy[] = {1, 2, 2, 1, -1, -2, -2, -1};
int sx, sy;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ )
if (g[i][j] == 'K')
sx = i, sy = j;
int hh = 0, tt = 0;
q[0] = {sx, sy};
memset(dist, -1, sizeof dist);
dist[sx][sy] = 0;
while (hh <= tt){
PII t = q[hh ++];
for (int i = 0; i < 8; i ++){
int a = t.x + dx[i], b = t.y + dy[i];
if (a < 0 || a >= n || b < 0 || b >= m) continue;
if (g[a][b] == '*') continue;
if (dist[a][b] != -1) continue;
if (g[a][b] == 'H') return dist[t.x][t.y] + 1;
dist[a][b] = dist[t.x][t.y] + 1;
q[++ tt] = {a, b};
}
}
return -1;
}
int main(){
cin >> m >> n;
for (int i = 0; i < n; i ++ ) cin >> g[i];
cout << bfs() << endl;
return 0;
}
七、抓住那头牛
由于农夫三种移动方式所需要的时间相同,考虑使用BFS。但是如果要使用BFS,问题的搜索空间不能是无限的, 能否框定出一个范围,使得最优解所经过的点一定在这个范围内呢?
可以证明,最优解所经过的点一定在
[
0
,
2
×
1
0
5
]
[0,2\times 10^5]
[0,2×105] 内:
令三种移动方式
X
→
X
−
1
X→X-1
X→X−1,
X
→
X
+
1
X→X+1
X→X+1,
X
→
2
X
X→2X
X→2X 分别为操作一、二、三
- 若某次执行操作一后位置小于 0 0 0,此时执行操作一、三只会离目标点更远,所以在最优解中只会执行操作二;但执行操作二与前面执行的操作一正好抵消,回到原来位置,还不如不执行操作一,故位置不会小于 0 0 0;
- 若目标点在当前位置的左边,此时执行操作二、三只会离目标点更远,所以在最优解中只会执行操作一;而目标点 ≤ 1 0 5 \le 10^5 ≤105,故在最优解中会执行操作二、三的点一定 ≤ 2 × 1 0 5 \le 2\times 10^5 ≤2×105 。
在框定了搜索范围后,我们便可使用BFS求出最小步数。
当然我们可以进一步缩小范围至 [ 0 , 1 0 5 ] [0,10^5] [0,105],因为先执行操作三再执行两次以上的操作一,不如先执行操作一再执行操作三。
代码实现:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2e5 + 10;
int n, k;
int q[N];
int dist[N];
int bfs(){
memset(dist, -1, sizeof dist);
dist[n] = 0;
q[0] = n;
int hh = 0, tt = 0;
while (hh <= tt){
int t = q[hh ++];
if (t == k) return dist[k];
if (t + 1 < N && dist[t + 1] == -1){
dist[t + 1] = dist[t] + 1;
q[++ tt] = t + 1;
}
if (t - 1 >= 0 && dist[t - 1] == -1){
dist[t - 1] = dist[t] + 1;
q[++ tt] = t - 1;
}
if (t * 2 < N && dist[t * 2] == -1){
dist[t * 2] = dist[t] + 1;
q[++ tt] = t * 2;
}
}
return -1; //本题一定有解,可以不用写这句
}
int main(){
cin >> n >> k;
cout << bfs() << endl;
return 0;
}