预告:我用两年写的新书《算法竞赛》,已于2022年2月交给清华大学出版社,预计于2022年7月出版。《算法竞赛》是一本“大全”,内容覆盖“基础-中级-高级”,篇幅700页左右。部分知识点的草稿已经在本博客发表。本篇博客节选自新书《算法竞赛》的“3.1.6 连通性判断”。
2018年蓝桥杯省赛真题
全球变暖 https://www.lanqiao.cn/problems/178/learning/
题目描述:有一张某海域 N×N 像素的照片,".“表示海洋、”#"表示陆地,如下图所示:
.......
.##....
.##....
....##.
..####.
...###.
.......
其中"上下左右"四个方向上连在一起的一片陆地组成一座岛屿。例如上图就有 2 座岛屿。
由于全球变暖导致海面上升,岛屿边缘一个像素的范围会被海水淹没。如果一块陆地像素与海洋相邻(上下左右四个相邻像素中有海洋),它就会被淹没。例如上图中的海域未来会变成如下样子:
.......
.......
.......
.......
....#..
.......
.......
请计算:照片中有多少岛屿会被完全淹没。
输入:第一行包含一个整数N (1≤N≤1000),以下N行N列代表一张海域照片。照片保证第 1 行、第 1 列、第N行、第N列的像素都是海洋。
输出:一个整数表示答案。
这是基本的连通性问题,计算步骤是:遍历一个连通块(找到这个连通块中所有的’#‘,并标记已经搜过,不用再搜);再遍历下一个连通块…;遍历完所有连通块,统计有多少个连通块。
连通性判断需要用暴力搜索解决:挨个搜连通块上的所有点,不遗漏一个;另外每个点只搜一次。这种简单的暴力搜索,用BFS和DFS都行,不仅很容易搜到所有点,而且每个点只搜一次,效率高。
回到本题,什么岛屿不会被完全淹没?若岛中有个陆地(称为高地),它周围都是陆地,那么这个岛不会被完全淹没。用DFS或BFS搜出有多少个岛(连通块),检查这个岛有没有高地,统计那些没有高地的岛(连通块)的数量,就是答案。
计算复杂度:因为每个像素点只用搜一次且必须至少搜一次,共 N 2 N^2 N2个点,复杂度 O ( N 2 ) O(N^2) O(N2),不可能更好了。
为了对比DFS和BFS,下面先用DFS实现,后面再用BFS实现。
做题时,一般用DFS判断连通性,DFS比BFS编码少。
DFS所有的像素点,若遇到’#‘,就继续DFS它周围的’#‘。把搜过的’#‘标记为已经搜过,不用再搜。统计那些没有高地的岛的数量,就是答案。
搜索时应该判断是不是出了边界。不过,题目已经说“照片保证第1行、第1列、第N行、第N列的像素都是海洋”,那么就不用判断边界了,到了边界,发现是水,DFS会自动停止搜索。
下面的代码,main()中每做一次DFS就是搜一个岛屿。代码中有两个重要变量。
(1)flag。在一次DFS中,如果发现这个岛有高地,置flag=1。如果这个岛中没有高地,那么有flag=0,统计答案ans++。
(2)vis[][]用于判断一个点是否被搜过。如果vis[][]==1,后面就不用重复搜了。代码第15行:
if(vis[nx][ny]==0 && mp[nx][ny]=='#')
判断vis[nx][ny]==0。如果没有这个判断,那么很多点会重复进行DFS,导致超时。这是一种剪枝技术,叫作“记忆化搜索”。
#include
using namespace std;
const int N = 1010;
char mp[N][N]; //地图
int vis[N][N]={0}; //标记是否搜过
int d[4][2] = {{0,1}, {0,-1}, {1,0}, {-1,0}}; //四个方向
int flag; //用于标记这个岛中是否被完全淹没
void dfs(int x, int y){
vis[x][y] = 1; //标记这个'#'被搜过。注意为什么放在这里
if( mp[x][y+1]=='#' && mp[x][y-1]=='#' &&
mp[x+1][y]=='#' && mp[x-1][y]=='#' )
flag = 1; //上下左右都是陆地,这是一个高地,不会淹没
for(int i = 0; i < 4; i++){ //继续DFS周围的陆地
int nx = x + d[i][0], ny = y + d[i][1];
if(vis[nx][ny]==0 && mp[nx][ny]=='#') //注意为什么要判断vis[][]
//继续DFS未搜过的陆地,目的是标记它们
dfs(nx,ny);
}
}
int main(){
int n; cin >> n;
for (int i = 0; i < n; i++) cin >> mp[i];
int ans = 0 ;
for(int i = 1; i <= n; i++) //DFS所有像素点
for(int j = 1; j <= n; j++)
if(mp[i][j]=='#' && vis[i][j]==0){
flag = 0; //假设这个岛被淹
dfs(i,j); //找这个岛中有没有高地,如果有,置flag=1
if(flag == 0) ans++; //这个岛被淹了,统计被淹没岛的数量
}
cout<<ans<<endl;
return 0;
}
BFS的思路比较简单。下面代码第34行,看到一个"#“后,就用BFS扩展它周围的”#",所有和它相连的"#"属于一个岛。然后按前面DFS提到的方法:找高地,判断是否淹没。
代码比DFS要复杂一点,因为用到了队列。这里直接用STL的queue,放进队列的是坐标点pair
#include
using namespace std;
const int N = 1010;
char mp[N][N];
int vis[N][N];
int d[4][2] = {{0,1}, {0,-1}, {1,0}, {-1,0}}; //四个方向
int flag;
void bfs(int x, int y) {
queue<pair<int, int>> q;
q.push({x, y});
vis[x][y] = 1; //标记这个'#'被搜过
while (q.size()) {
pair<int, int> t = q.front();
q.pop();
int tx = t.first, ty = t.second;
if( mp[tx][ty+1]=='#' && mp[tx][ty-1]=='#' &&
mp[tx+1][ty]=='#' && mp[tx-1][ty]=='#' )
flag = 1; //上下左右都是陆地,不会淹没
for (int i = 0; i < 4; i++) { //扩展(tx,ty)的4个邻居
int nx = tx + d[i][0], ny = ty + d[i][1];
if(vis[nx][ny]==0 && mp[nx][ny]=='#'){ //把陆地放进队列
vis[nx][ny] = 1; //注意:这一句必不可少
q.push({nx, ny});
}
}
}
}
int main() {
int n; cin >> n;
for (int i = 0; i < n; i++) cin >> mp[i];
int ans = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
if (mp[i][j] == '#' && vis[i][j]==0) {
flag = 0;
bfs(i, j);
if(flag == 0) ans++; //这个岛全部被淹,统计岛的数量
}
cout << ans << endl;
return 0;
}