扫描网格,每遇到未访问水格就用八方向 BFS 淹掉整个连通块并把答案加一。
OJ: luogu
题目 ID: P1596
难度:普及-
标签:BFSflood fill连通块网格
创建: 2026-07-16 18:01
更新: 2026-10-09 21:09
形式化题目
给定一个 W 或干地 .。两个格子若上下左右或斜对角相邻且都是水,则属于同一个水塘;水塘是八方向连通的水格的极大集合。求网格中水塘的数量。
思路
数连通块没有更弱的直接做法:任何正确算法都必须把每个水塘完整遍历一遍,所以洪水填充(Flood Fill)既是朴素想法也是最终做法,不存在"慢暴力 -> 快优化"的演进,这里不单独展示暴力代码。
先看八方向相邻的含义。下面这张 4x4 网格演示一个小样例:
初始网格 扫描到 (1,1):水塘数 1,BFS 淹没左上连通块 继续扫描
WW.. **.. **..
W.W. *.*. *.*.
.... .... ....
..W. ..W. ..**观察两点:
- 图中有两个水塘:左上连通块
(1,1),(1,2),(2,1),(2,3)和右下(4,3)。(1,2)与(2,3)只是斜对角接触,八方向下属于同一个水塘;若只算上下左右,它们就会分开。所以方向数组必须包含四条对角线。 - 扫描到
(1,1)时答案加一,BFS 把整个水塘的格子标记掉;之后扫描再遇到(4,3)时它仍然是W,说明它是一个新水塘,答案再加一。扫描时遇到W只可能来自一个还没统计过的水塘,这就是计数正确性的来源。
实现上直接修改网格:BFS 每访问一个水格就把它改成 .,这一步同时完成了访问标记,之后的扫描和扩展都不会再处理它,省掉单独的 vis 数组。入队前标记而不是出队时标记,保证每个格子最多入队一次。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-27 00:00
* update_at: 2026-08-13 13:33
*/
/* P1596 [USACO10OCT] Lake Counting S */
/* 扫描网格,每遇到未访问水格就用八方向 BFS 淹掉整个连通块并把答案加一。 */
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 105;
int n, m;
char g[MAXN][MAXN]; // 网格:'W' 是水,'.' 是干地
int dx[8] = { -1, -1, -1, 0, 0, 1, 1, 1 }; // 八方向:上下左右加四条对角线
int dy[8] = { -1, 0, 1, -1, 1, -1, 0, 1 };
// BFS 从 (sx, sy) 出发,把整个水塘里的 'W' 全部标记成 '.'。
void flood_fill(int sx, int sy) {
queue<pair<int, int>> q;
g[sx][sy] = '.';
q.push(make_pair(sx, sy));
while (!q.empty()) {
int x = q.front().first;
int y = q.front().second;
q.pop();
// 向八个方向扩展,未访问的水格直接标记后入队。
for (int i = 0; i < 8; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && g[nx][ny] == 'W') {
g[nx][ny] = '.'; // 入队前标记,避免同一个格子重复入队
q.push(make_pair(nx, ny));
}
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> (g[i] + 1);
}
int ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (g[i][j] == 'W') { // 遇到未访问的水格,必然是一个新水塘
ans++;
flood_fill(i, j);
}
}
}
cout << ans << "\n";
return 0;
}STL 写法
上面的 main.cpp 用固定数组 char g[105][105] 存网格,队列里放 pair<int, int>,格子坐标从 1 开始。让容器来管这两件事也可以:网格用 vector<string>,每行一个 string,读入直接 cin >> g[i],不必再写 cin >> (g[i] + 1) 这种带下标偏移的写法;坐标顺带变成 0 下标,越界判断也跟着从 nx >= 1 && nx <= n 换成 nx >= 0 && nx < n。
队列元素是压平后的下标 x * m + y:一个格子只用一个整数表示,出队时用 id / m、id % m 还原行列,省掉了反复构造 pair<int, int>。八方向的 dx / dy 和“入队前就把 W 改成 .”这两点与 main.cpp 完全一致,所以每个格子仍然最多入队一次,复杂度不变。
对应的 cppbook 章节:vector:能改变长度的数组、queue:先进先出
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-09 21:09
* update_at: 2026-10-09 21:09
*/
// main-stl.cpp:P1596 [USACO10OCT] Lake Counting S(STL 写法)。
// 算法和 main.cpp 完全一样:扫到未访问的 'W' 就说明是新水塘,从它开始八方向
// 洪水填充,把整个水塘淹掉,答案加一。
// 差别只在“网格和队列怎么表达”:
// main.cpp 用固定数组 char g[105][105],队列存 pair<int, int>;
// 这里网格用 vector<string>,每行一个 string,读入直接 cin >> g[i],
// 队列存压平后的下标 x * m + y,一个整数就是一个格子,不用构造 pair。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll n, m; // 网格的行数、列数
vector<string> g; // g[x][y]:'W' 是水,'.' 是干地;0 下标,压平编号是 x * m + y
ll dx[8] = { -1, -1, -1, 0, 0, 1, 1, 1 }; // 八方向:上下左右加四条对角线
ll dy[8] = { -1, 0, 1, -1, 1, -1, 0, 1 };
queue<ll> q; // BFS 队列,元素是压平下标 x * m + y
// 读入 n、m 和 n 行网格
void read_input() {
cin >> n >> m;
g.assign(n, string());
for (ll i = 0; i < n; i++) {
cin >> g[i];
}
}
// 从 (sx, sy) 出发淹掉整个水塘:沿途的 'W' 原地改成 '.'
void flood_fill(ll sx, ll sy) {
g[sx][sy] = '.';
q.push(sx * m + sy);
while (!q.empty()) {
ll id = q.front();
q.pop();
ll x = id / m; // 压平下标还原成行列
ll y = id % m;
for (ll i = 0; i < 8; i++) {
ll nx = x + dx[i];
ll ny = y + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) {
continue; // 越界
}
if (g[nx][ny] != 'W') {
continue; // 干地,或者已经属于淹过的水塘
}
g[nx][ny] = '.'; // 入队前就标记,保证每个格子最多入队一次
q.push(nx * m + ny);
}
}
}
// 扫描全网格:遇到的 'W' 只可能来自还没统计过的水塘
ll count_ponds() {
ll ans = 0;
for (ll i = 0; i < n; i++) {
for (ll j = 0; j < m; j++) {
if (g[i][j] == 'W') {
ans++;
flood_fill(i, j);
}
}
}
return ans;
}
void output(ll ans) {
cout << ans << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
output(count_ponds());
return 0;
}复杂度
- 时间:每个格子至多入队、出队一次,加一遍全网格扫描,
。 - 空间:网格数组与 BFS 队列,
。
总结
统计连通块是一类固定套路:发现一个未访问的目标点,答案加一,再搜索并标记整个连通块。本题只是把"相邻"定义为八方向,把网格本身当成图来遍历。rbook 的《图的遍历》与模板 connected-components 讲的就是同一套"枚举点 -> 发现新连通块 -> DFS/BFS 淹没"的结构。
图示解析
这张 ASCII 图展示整道题的解题路线:
题意:八方向相邻的 'W' 构成一个水塘,数水塘个数
|
| 关键观察:扫描到的第一个未访问 'W' 必然属于新水塘
v
Flood Fill(main.cpp)
扫描全网格
|-- 格子是 '.':跳过
`-- 格子是 'W':答案 +1,从它开始 BFS
BFS 规则:向 8 个方向(含对角)扩展
入队前把 'W' 改成 '.'(原地标记,避免重复处理)
|
v
答案 = BFS 启动的次数
每个格子最多进出队一次:时间 O(n*m),空间 O(n*m)图中上半部分解释"为什么扫到 W 就能确定是新水塘"——之前的 BFS 已经把所有旧水塘变成 .;下半部分是 BFS 的实现要点。整道题的困难只在两个细节:方向必须是八个(含对角线),以及入队前就标记防止重复。