【AcWing算法提高课】2.1.1BFS中的Flood Fill和最短路模型

零、BFS的两大模型和使用情景

BFS两大模型:

  1. 最短距离模型 (如基础课“走迷宫”一题):求方格矩阵中,求以某个点为起点,走到目标终点的最短距离
  2. 最小步数模型 (如基础课“八数码”一题):把矩阵看成一种状态,求从一种状态到另一种状态的最小变换次数

BFS使用情景:

  1. “求最小”,即BFS第一次搜到目标结果时一定是最小值
  2. 基于迭代,相比于DFS不会爆栈 (算法题中一般默认栈空间为1M)

一、Flood Fill 概述

Flood Fill (洪水填充法),可在线性时间复杂度内找到某个点所在的连通块。

譬如在一个地形图中,每个小方格有两种高度:“凸起”和“凹进”,为了找到一块相连通的"凹进" (称其为“洼地”),我们可以先找到任意一块“凹进”,开始注水 (水的总量无限,不会流干)。凭生活经验,水肯定会向四周同是“凹进”的地方蔓延,且不会蔓延至更高的“凸起”。当水不再蔓延时,水所有覆盖的地方既是一块“洼地”。这个过程如下图所示:

【AcWing算法提高课】2.1.1BFS中的Flood Fill和最短路模型_第1张图片
“水”一次次蔓延的过程可以用BFS或者DFS模拟,BFS是“一圈一圈向外蔓延”,DFS则是“沿着一条路线一直流”。由于DFS在深度较大时可能会爆栈,我们一般用BFS模拟蔓延的过程。

二、池塘计数

1097.池塘计数 题目链接

本题要求找到所有相连的“W”块,可以使用Flood Fill方法实现。从前往后寻找第一个“W”块,找到后将池塘数量 + 1 +1 +1 ,并对其“注水”,使得所有与其相连的“W”块均被覆盖。重复以上过程,直到所有“W”块已被覆盖。

代码实现:

#include 
#include 
#include 

#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;
}

三、城堡问题

1098.城堡问题 题目链接

本题需要统计房间总数和最大房间的面积,与上题基本相同。注意本题中“墙壁”用二进制方法给出,在判断联通时需要使用位运算。在统计房间面积时,在入队或出队时加一均可,下面给出的代码是在出队时加一。

代码实现:

#include 
#include 
#include 

#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;
}

四、山峰和山谷

1106.山峰和山谷 题目链接

本题需要统计山峰和山谷的个数,其中山峰指的是周围没有比它还高的一片区域,山谷是周围没有比它还低的一片区域。在Flood Fill过程中,如果周围的方格高度不同,需要判断它比当前区域高还是低,如果更高,当前区域不会是山峰;如果更低,当前区域不会是山谷。

代码实现:

#include 
#include 
#include 

#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;
}

五、迷宫问题

1076.迷宫问题 题目链接

本题在“走迷宫”的基础上要求记录路径。在BFS时,记录每个点是由哪个点走到的,最后逆序输出即可。当然也可反向BFS,将终点和起点互换,记录反向路径后直接正向输出。

代码实现:

#include 
#include 
#include 

#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;
}

六、武士风度的牛

188.武士风度的牛 题目链接

普通的BFS问题,马走日。

代码实现:

#include 
#include 
#include 

#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;
}

七、抓住那头牛

1100.抓住那头牛 题目链接

由于农夫三种移动方式所需要的时间相同,考虑使用BFS。但是如果要使用BFS,问题的搜索空间不能是无限的, 能否框定出一个范围,使得最优解所经过的点一定在这个范围内呢?

可以证明,最优解所经过的点一定在 [ 0 , 2 × 1 0 5 ] [0,2\times 10^5] [0,2×105] 内:
令三种移动方式 X → X − 1 X→X-1 XX1 X → X + 1 X→X+1 XX+1 X → 2 X X→2X X2X 分别为操作一、二、三

  1. 若某次执行操作一后位置小于 0 0 0,此时执行操作一、三只会离目标点更远,所以在最优解中只会执行操作二;但执行操作二与前面执行的操作一正好抵消,回到原来位置,还不如不执行操作一,故位置不会小于 0 0 0
  2. 若目标点在当前位置的左边,此时执行操作二、三只会离目标点更远,所以在最优解中只会执行操作一;而目标点 ≤ 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 
#include 
#include 

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;
}

你可能感兴趣的:(AcWing算法提高课,算法)