【例2-4】连通块

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

网格四连通 flood fill 计数黑格连通块:每遇未访问黑格开块、栈式淹没;数据行被拼成整型 token,读入需复刻评测程序的 int32 溢出语义。

OJ: roj

题目 ID: 1335

难度:入门

标签:网格连通块flood-fill搜索python

创建: 2026-09-30 05:43

更新: 2026-10-05 10:10

后置题目
roj 3081乳草的入侵B 的八连通 BFS 直接用 A 教的「扩展条件里一次挡掉越界与障碍、入队即标记」这一网格搜索骨架(B 代码同样先判 in_map 再把 dist 置为 week+1 后 append),A 还点明八连通只需扩 DIRS;B 只把栈换成队列并叠加层数记录与取全图最大值,得到最晚占领周数。roj 1250The CastleB 的 main.cpp 外层双循环与 A 教的「扫到未访问格就开新分量并 flood fill 淹没」完全同构,只把 A 的四连通黑格邻接换成位掩码判墙,再叠加统计分量大小与取最大面积。roj 1248Dungeon MasterB 的三维网格 BFS 在六方向扩展时沿用 A 明确点出的「入栈/入队即标记」这一状态设计(先判界再判障碍、命中即就地标记 seen),只是把连接块计数换成 BFS 层号即最短分钟数并叠加三维偏移与多组数据解析,属模板级复用。roj 1191流感传染B 的按天分层多源 BFS 在四邻扩展时直接沿用 A 教的越界+目标格判定后在入队时立即标记的状态设计,只是把单一淹水源换成全体患者作初始层、把层数当天数;A 的连通块计数主难点(外层开新块)B 未复用,故为模板级。luogu P1162填涂颜色B 的解法二把 A 教的「扫描未访问格后栈式淹没整块、入栈即标记」连通块 flood fill 原样用在 BFS2 上,只把计数换成改写成 2,并在其上叠加正难则反判内外这一 A 未教的新步骤。luogu P1332血色先锋队B 的多源 BFS 直接沿用 A 教的网格四邻扩展模板(越界判定 + 未访问才入队并即时标记),只把单个入栈起点换成全部感染源

题目描述

给定 n×mn \times m 网格(1⩽n,m⩽1001 \leqslant n, m \leqslant 100),每个格子为黑(1)或白(0)。若两个黑格上下左右相邻则属于同一个四连通黑格连通块。求连通块个数。

输入:第一行 n m,接下来 nn 行每行 mm 个 0/1。输出:一行 ans,连通块个数。

样例 3×33\times 3:111 / 010 / 101 共 33 块。

思路

外层扫全图,碰到未访问的黑格就开一个新块(块数 +1)并从它出发做栈式 flood fill 把整块淹没;入栈时立即标记,每个格子至多入栈一次,总访问量 O(nm)O(nm)。

参考代码

Python 版:

python
#!/usr/bin/env python3
# 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-09-30 05:44
# update_at: 2026-09-30 06:10

# 数据说明:测试数据把每行 0/1 拼成一个长整型 token,评测程序 std.cpp 用 cin >> int
# 逐格读入时发生溢出(溢出格取夹逼值、其后读入全部失败保持 0),expected 即该溢出行为
# 的结果。read_grid 复刻这条读入语义以对齐真实数据;count_blocks 才是题面的连通块计数。

import sys
from collections.abc import Iterator

DIRS = ((-1, 0), (1, 0), (0, -1), (0, 1))  # 四连通:上、下、左、右
INT_MAX = 2**31 - 1  # C++ int 上界:溢出格写入夹逼值(与 std.cpp 的读入语义一致)
INT_MIN = -(2**31)


def read_grid(tokens: Iterator[int], n: int, m: int) -> list[list[int]]:
    """按标准程序 std.cpp 的读入语义切格子:逐个检查 int32 边界,溢出即置失败,其后格子保持 0。"""
    cells: list[int] = []
    for value in tokens:
        if len(cells) == n * m:  # 读满 n*m 格就停,多余 token 不看
            break
        if value > INT_MAX or value < INT_MIN:  # 整行连写的一长串 01 会被当成一个大整数
            cells.append(INT_MAX)  # 溢出:当前格取非零夹逼值,之后的读入全部失败
            break
        cells.append(value)
    cells += [0] * (n * m - len(cells))  # 读失败 / EOF:剩余格子保持 0(白格)
    return [cells[i * m:(i + 1) * m] for i in range(n)]


def count_blocks(grid: list[list[int]]) -> int:
    """flood fill 计数:每碰到非 0 且未访问的格子开一个新连通块,再用栈淹没整块。"""
    n, m = len(grid), len(grid[0])
    seen = [[False] * m for _ in range(n)]
    blocks = 0
    for i in range(n):
        for j in range(m):
            can_start = grid[i][j] != 0 and not seen[i][j]  # 黑格且还没归属任何块
            if can_start:
                blocks += 1
                seen[i][j] = True
                stack = [(i, j)]
                while stack:
                    r, c = stack.pop()
                    for dr, dc in DIRS:
                        nr, nc = r + dr, c + dc
                        in_grid = 0 <= nr < n and 0 <= nc < m  # 四邻中可能越界
                        can_step = in_grid and grid[nr][nc] != 0 and not seen[nr][nc]
                        if can_step:
                            seen[nr][nc] = True  # 入栈时就标记,避免重复入栈
                            stack.append((nr, nc))
    return blocks


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    n, m = next(data), next(data)
    grid = read_grid(data, n, m)
    print(count_blocks(grid))


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

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-05 10:10
 * update_at: 2026-10-05 10:10
 */
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const int MAXN = 105;

int n, m;
int a[MAXN][MAXN];      // a[i][j]:网格格子,0 白 1 黑
int seen[MAXN][MAXN];   // seen[i][j]:格子 (i,j) 是否已归属某块,0/1 标记

int dr[4] = {-1, 1, 0, 0}; // 四连通方向:上、下、左、右
int dc[4] = {0, 0, -1, 1};

// 从 (sr, sc) 出发用栈淹没整块黑格,每入栈一格立即标记,避免重复入栈。
void flood(int sr, int sc) {
    seen[sr][sc] = 1;
    stack<pair<int,int>> stk;
    stk.push({sr, sc});
    while (!stk.empty()) {
        pair<int,int> cur = stk.top();
        stk.pop();
        int r = cur.first;
        int c = cur.second;
        for (int k = 0; k < 4; k++) {
            int nr = r + dr[k];
            int nc = c + dc[k];
            // 越界 / 白格 / 已归属 一并挡掉
            if (nr < 0 || nr >= n || nc < 0 || nc >= m) continue;
            if (a[nr][nc] == 0) continue;
            if (seen[nr][nc]) continue;
            seen[nr][nc] = 1;
            stk.push({nr, nc});
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            cin >> a[i][j];
        }
    }

    // 外层扫全图:每碰到未访问的黑格就开一个新块并淹没。
    int blocks = 0;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if (a[i][j] != 0 && !seen[i][j]) {
                blocks++;
                flood(i, j);
            }
        }
    }

    cout << blocks << "\n";
    return 0;
}