[USACO10OCT] Lake Counting S

GitHub跳转原题关系图返回列表

扫描网格,每遇到未访问水格就用八方向 BFS 淹掉整个连通块并把答案加一。

OJ: luogu

题目 ID: P1596

难度:普及-

标签:BFSflood fill连通块网格

创建: 2026-07-16 18:01

更新: 2026-10-09 21:09

形式化题目

给定一个 n×mn \times m 的字符网格,每个格子是水 W 或干地 .。两个格子若上下左右或斜对角相邻且都是水,则属于同一个水塘;水塘是八方向连通的水格的极大集合。求网格中水塘的数量。

思路

数连通块没有更弱的直接做法:任何正确算法都必须把每个水塘完整遍历一遍,所以洪水填充(Flood Fill)既是朴素想法也是最终做法,不存在"慢暴力 -> 快优化"的演进,这里不单独展示暴力代码。

先看八方向相邻的含义。下面这张 4x4 网格演示一个小样例:

text
初始网格        扫描到 (1,1):水塘数 1,BFS 淹没左上连通块       继续扫描
WW..            **..                                          **..
W.W.            *.*.                                          *.*.
....            ....                                          ....
..W.            ..W.                                          ..**

观察两点:

  1. 图中有两个水塘:左上连通块 (1,1),(1,2),(2,1),(2,3) 和右下 (4,3)。(1,2) 与 (2,3) 只是斜对角接触,八方向下属于同一个水塘;若只算上下左右,它们就会分开。所以方向数组必须包含四条对角线。
  2. 扫描到 (1,1) 时答案加一,BFS 把整个水塘的格子标记掉;之后扫描再遇到 (4,3) 时它仍然是 W,说明它是一个新水塘,答案再加一。扫描时遇到 W 只可能来自一个还没统计过的水塘,这就是计数正确性的来源。

实现上直接修改网格:BFS 每访问一个水格就把它改成 .,这一步同时完成了访问标记,之后的扫描和扩展都不会再处理它,省掉单独的 vis 数组。入队前标记而不是出队时标记,保证每个格子最多入队一次。

代码

cpp
/**
 * 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:先进先出

cpp
/**
 * 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;
}

复杂度

  • 时间:每个格子至多入队、出队一次,加一遍全网格扫描,O(nm)O(nm)。
  • 空间:网格数组与 BFS 队列,O(nm)O(nm)。

总结

统计连通块是一类固定套路:发现一个未访问的目标点,答案加一,再搜索并标记整个连通块。本题只是把"相邻"定义为八方向,把网格本身当成图来遍历。rbook 的《图的遍历》与模板 connected-components 讲的就是同一套"枚举点 -> 发现新连通块 -> DFS/BFS 淹没"的结构。

图示解析

这张 ASCII 图展示整道题的解题路线:

text
题意:八方向相邻的 'W' 构成一个水塘,数水塘个数
        |
        | 关键观察:扫描到的第一个未访问 'W' 必然属于新水塘
        v
Flood Fill(main.cpp)
   扫描全网格
   |-- 格子是 '.':跳过
   `-- 格子是 'W':答案 +1,从它开始 BFS
        BFS 规则:向 8 个方向(含对角)扩展
        入队前把 'W' 改成 '.'(原地标记,避免重复处理)
        |
        v
答案 = BFS 启动的次数
每个格子最多进出队一次:时间 O(n*m),空间 O(n*m)

图中上半部分解释"为什么扫到 W 就能确定是新水塘"——之前的 BFS 已经把所有旧水塘变成 .;下半部分是 BFS 的实现要点。整道题的困难只在两个细节:方向必须是八个(含对角线),以及入队前就标记防止重复。