[USACO10OCT] Lake Counting S

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

OJ: luogu

题目 ID: P1596

难度:普及-

标签:BFSflood fill连通块网格

日期: 2026-07-16 18:01

形式化题目

给定一个 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;
}

复杂度

  • 时间:每个格子至多入队、出队一次,加一遍全网格扫描,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 的实现要点。整道题的困难只在两个细节:方向必须是八个(含对角线),以及入队前就标记防止重复。