今天遇到一个有趣的算法题目——数池塘问题。题目描述很简单:给定一个N×M的二维矩阵,每个格子要么是'W'表示有水,要么是'.'表示陆地。我们需要统计其中"池塘"的数量,这里的池塘定义为由相邻'W'组成的区域,相邻包括八个方向(上下左右加四个对角线)。
这个问题本质上是一个经典的连通区域计数问题,在图像处理、游戏开发等领域都有广泛应用。解决这类问题最常用的两种算法就是深度优先搜索(DFS)和广度优先搜索(BFS),它们都属于Flood Fill(泛洪填充)算法的具体实现。
提示:Flood Fill算法就像用油漆桶工具填充一个封闭区域,它会从起点开始,向四周扩散填充,直到遇到边界为止。
DFS的实现思路非常直观:从遇到的第一个'W'开始,递归地向八个方向探索,把所有连通的'W'都标记为已访问(这里通过改为'.'实现),这样就完成了一个池塘的标记,计数器加1。
cpp复制void dfs(int x,int y) {
mp[x][y]='.'; // 标记为已访问
for(int i=0; i<8; i++) {
int tx=x+dx[i];
int ty=y+dy[i];
if(mp[tx][ty]=='W') {
dfs(tx,ty); // 递归处理相邻格子
}
}
}
这里使用了一个小技巧——方向数组,它定义了八个方向的坐标偏移量:
cpp复制int dx[] = {1,1,0,-1,-1,-1,0,1}; // x方向偏移
int dy[] = {0,1,1,1,0,-1,-1,-1}; // y方向偏移
这种写法比写八个if语句更简洁,也更容易修改。方向顺序通常是从正右开始顺时针旋转,但具体顺序不影响结果。
在实际代码中,我们需要注意数组越界问题。DFS版本中虽然没有显式检查边界,但题目保证输入数据边缘都是'.',所以可以省略边界检查。但在更严格的场景下,应该添加:
cpp复制if(tx<0 || ty<0 || tx>=n || ty>=m) continue;
BFS使用队列来实现,它像水波纹一样一层层向外扩展。从起点开始,先处理所有相邻的'W',再处理这些'W'的相邻'W',依此类推。
cpp复制void bfs(int x,int y) {
queue<PII> q;
q.push({x,y});
mp[x][y]='.';
while(!q.empty()) {
PII t = q.front();
q.pop();
for(int i=0; i<8; i++) {
int tx = t.first+dx[i];
int ty = t.second+dy[i];
if(tx>=0 && ty>=0 && tx<n && ty<m && mp[tx][ty]=='W') {
mp[tx][ty]='.';
q.push({tx,ty});
}
}
}
}
两种算法的时间复杂度都是O(N×M),因为它们每个格子只访问一次。但实际应用中:
原代码使用双重循环逐个字符读取输入,这在竞赛中可能不够高效。可以考虑以下优化:
cpp复制for(int i=0; i<n; i++)
scanf("%s", mp[i]); // 整行读取
题目中使用了全局变量存储地图和计数,这在算法题中是常见做法,可以简化参数传递。但在大型项目中应避免过度使用全局变量。
cpp复制#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> PII;
const int N=1e3+5;
char mp[N][N];
int dx[]={1,1,0,-1,-1,-1,0,1};
int dy[]={0,1,1,1,0,-1,-1,-1};
int n,m,ans;
void bfs(int x,int y){
queue<PII> q;
q.push({x,y});
mp[x][y]='.';
while(!q.empty()){
auto t=q.front(); q.pop();
for(int i=0;i<8;i++){
int tx=t.first+dx[i], ty=t.second+dy[i];
if(tx>=0&&tx<n&&ty>=0&&ty<m&&mp[tx][ty]=='W'){
mp[tx][ty]='.';
q.push({tx,ty});
}
}
}
}
int main(){
cin>>n>>m;
for(int i=0;i<n;i++) scanf("%s",mp[i]);
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
if(mp[i][j]=='W'){
bfs(i,j);
ans++;
}
cout<<ans<<endl;
return 0;
}
如果忘记将访问过的'W'标记为'.',会导致程序在相邻的'W'之间无限循环。这是最常见的错误之一。
虽然题目数据保证边界是安全的,但在其他类似问题中,忘记检查数组边界会导致段错误。建议养成习惯总是检查边界。
方向数组定义错误会导致漏掉某些方向或重复处理。可以打印出dx,dy的值来验证:
cpp复制for(int i=0;i<8;i++)
cout<<"dx="<<dx[i]<<" dy="<<dy[i]<<endl;
对于更大的网格(比如1000×1000),可以考虑以下优化:
本题是八方向连通,有些题目可能要求四方向(上下左右)。只需修改方向数组即可:
cpp复制// 四方向版本
int dx[] = {1,0,-1,0};
int dy[] = {0,1,0,-1};
如果需要统计每个池塘的大小,可以在DFS/BFS中增加一个计数器:
cpp复制int dfs(int x,int y){
mp[x][y]='.';
int area = 1;
for(int i=0;i<8;i++){
int tx=x+dx[i], ty=y+dy[i];
if(mp[tx][ty]=='W')
area += dfs(tx,ty);
}
return area;
}
Flood Fill算法虽然简单,但非常实用。掌握它的各种变种可以解决很多网格类问题。在实际编程中,我更喜欢使用BFS实现,因为它更直观且没有栈溢出风险。对于初学者,建议先理解DFS版本,再过渡到BFS版本。