跳转至

P1162 填涂颜色

题目描述

由数字 \(0\) 组成的方阵中,有一任意形状的由数字 \(1\) 构成的闭合圈。现要求把闭合圈内的所有空间都填写成 \(2\)。例如:\(6\times 6\) 的方阵(\(n=6\)),涂色前和涂色后的方阵如下:

如果从某个 \(0\) 出发,只向上下左右 \(4\) 个方向移动且仅经过其他 \(0\) 的情况下,无法到达方阵的边界,就认为这个 \(0\) 在闭合圈内。闭合圈不一定是环形的,可以是任意形状,但保证闭合圈内\(0\) 是连通的(两两之间可以相互到达)。

0 0 0 0 0 0
0 0 0 1 1 1
0 1 1 0 0 1
1 1 0 0 0 1
1 0 0 1 0 1
1 1 1 1 1 1
0 0 0 0 0 0
0 0 0 1 1 1
0 1 1 2 2 1
1 1 2 2 2 1
1 2 2 1 2 1
1 1 1 1 1 1

输入格式

每组测试数据第一行一个整数 \(n(1 \le n \le 30)\)

接下来 \(n\) 行,由 \(0\)\(1\) 组成的 \(n \times n\) 的方阵。

方阵内只有一个闭合圈,圈内至少有一个 \(0\)

输出格式

已经填好数字 \(2\) 的完整方阵。

输入输出样例 #1

输入 #1

6
0 0 0 0 0 0
0 0 1 1 1 1
0 1 1 0 0 1
1 1 0 0 0 1
1 0 0 0 0 1
1 1 1 1 1 1

输出 #1

0 0 0 0 0 0
0 0 1 1 1 1
0 1 1 2 2 1
1 1 2 2 2 1
1 2 2 2 2 1
1 1 1 1 1 1

说明/提示

对于 \(100\%\) 的数据,\(1 \le n \le 30\)

📌 题目分析

给定数据:

n × n 的方阵 (1 ≤ n ≤ 30)

由数字 01 组成,1 构成了一个闭合圈。要求把圈内的 0 全部变成 2 并输出最终的矩阵。

👉 本质:

区分二维网格中“被包围的区域”与“未被包围的区域”

👉 类型:

逆向思维 / Flood Fill (泛洪算法) / 广度优先搜索 (BFS)

🧠 解题思路

1️⃣ 为什么要用逆向思维?

如果我们直接去找“被 1 包围的 0”,判定条件会非常复杂,因为闭合圈的形状是不规则的,你很难判断一个 0 到底是不是真的在圈内。

但是,找“圈外的 0”极其简单! 圈外的 0 有一个绝对的特征:它们一定能和矩阵的边界相连通。 所以,只要我们把圈外所有的 0 全部找出来并打上标记,剩下的那些没被打标记的 0,必定就是被困在圈内的!

2️⃣ 核心技巧:矩阵外扩 (Padding)

如果我们直接在原矩阵的边界上找 0 进行搜索,需要写四个 for 循环分别遍历上下左右四条边,非常繁琐,而且容易漏掉某些角落。

神仙操作:我们在原有的 n × n 矩阵外面,人为地加上一圈 0。 原来矩阵的下标是 1n。我们把数组稍微开大一点,包含第 0 行、第 0 列、第 n+1 行、第 n+1 列,并且默认全为 0。 这样,矩阵外部所有的 0 都连成了一片“汪洋大海”。我们只需要从坐标 (0, 0) 开始进行一次 Flood Fill(泛洪搜索),就能把所有圈外的 0 全部找出来!

3️⃣ 状态映射

定义数组的值: * 1:墙壁(闭合圈本身)。 * 0:未被访问过的原始空格。 * 3(或者其他数字):我们在搜索时,把圈外的 0 全部染成 3

搜索结束后,遍历原矩阵的 1n 范围: * 如果遇到 3,说明是外面的,输出 0。 * 如果遇到 1,说明是墙壁,输出 1。 * 如果遇到 0,说明搜索没波及到它,它是圈内的,输出 2


💻 C++代码

#include <iostream>
#include <queue>
using namespace std;

int n;
// 数组开到 35,利用全局数组自动初始化为 0 的特性,天然形成了“外扩圈”
int grid[35][35];

// 方向数组:上下左右
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};

// 广度优先搜索 (Flood Fill)
void bfs(int startX, int startY) {
    queue<pair<int, int>> q;
    q.push({startX, startY});
    grid[startX][startY] = 3; // 把圈外的 0 标记为 3

    while (!q.empty()) {
        int x = q.front().first;
        int y = q.front().second;
        q.pop();

        for (int i = 0; i < 4; i++) {
            int nx = x + dx[i];
            int ny = y + dy[i];

            // 注意边界:包含扩充的 0 到 n+1
            // 只有遇到原始的 0 才会继续蔓延
            if (nx >= 0 && nx <= n + 1 && ny >= 0 && ny <= n + 1 && grid[nx][ny] == 0) {
                grid[nx][ny] = 3; // 染色
                q.push({nx, ny});
            }
        }
    }
}

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

    cin >> n;

    // 输入数据,注意下标从 1 开始,正好空出了 0 行 0 列
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            cin >> grid[i][j];
        }
    }

    // 从我们人为制造的“超级起点” (0,0) 开始泛洪
    bfs(0, 0);

    // 按要求输出结果
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            if (grid[i][j] == 0) {
                cout << 2 << " "; // 没被染色的 0 就是圈内的
            } else if (grid[i][j] == 3) {
                cout << 0 << " "; // 被染色的 3 还原成圈外的 0
            } else {
                cout << 1 << " "; // 墙壁 1 保持不变
            }
        }
        cout << "\n";
    }

    return 0;
}

⚠️ 易错点

❌ 1. 数组越界与边界条件

在使用 Padding 技巧时,矩阵的实际有效范围扩大到了 0n+1。 所以在 bfs 函数中判定是否越界时,必须写成 nx <= n + 1ny <= n + 1。如果还是习惯性地写 nx < n,会导致外扩的那一圈被截断,洪水无法绕过闭合圈流到另一边。

❌ 2. 搜索到了圈内

如果不用 Padding 技巧,而是从内部随便找一个 0 开始搜,你根本不知道这个 0 是在圈内还是圈外。核心法则是:必须从绝对在外部的坐标起手

❌ 3. 连通性判定错误

这道题目明确了“从某个 0 出发,只向上下左右 4 个方向移动”。对角线上的 1 是可以完美封死路线的。方向数组切记只能写 4 个方向,写成 8 方向洪水就会顺着对角线缝隙“漏”进圈内,把里面的 0 全染了。


🚀 一句话总结

正面强攻找圈内太难,不如在矩阵外围加一圈 0,从最外层放水把外面的 0 全淹了,剩下的干旱区就是圈内!

🔥 补充

硬核知识点:虚拟源点 (Dummy Node) 与边界扩充 (Padding)

这道题使用的技巧在图论和计算机图形学中非常经典。 * 边界扩充 (Padding):在卷积神经网络 (CNN) 处理图像时,为了不忽略边缘像素的特征,常常会在图像最外圈补一圈 0(即 Zero-padding)。这与我们在算法题中补一圈 0 防止搜索死角的思想如出一辙。 * 虚拟源点 (Dummy Node):当一个图有多个起点需要同时进行 BFS(比如求多个基地的最短距离),最暴力的做法是跑多次 BFS。而最高效的做法是:凭空创造一个“虚拟坐标”,让它到所有实际起点的距离都为 0。这就相当于把多个起点的多源 BFS,强行降维成了一个单源 BFS。本题中坐标 (0,0) 其实就充当了所有外部区域的“虚拟超级源点”。


我的做法dfs

#include<iostream>
#include<vector>
using namespace std;

int n, m;
int mp[20][20];
int dx[4] = { 1,-1,0,0};
int dy[4] = { 0,0,1,-1 };
bool vis[20][20];

void floodfill(int x,int y) {
    // 基础边界检查
    if (x < 0 || y < 0 || x > n + 1 || y > n + 1) return;
    // 关键修正:如果是墙(1)或者已经访问过,就停止搜索
    if (mp[x][y] == 1 || vis[x][y]) return;
    vis[x][y] = 1;
    mp[x][y] = 3;

    for (int i = 0;i < 4;i++) {
        int nx = x + dx[i], ny = y + dy[i];
        floodfill(nx, ny);
    }
    return;
}

int main() {
    cin >> n ;
    for (int i = 1;i <= n;i++) {
        for (int j = 1;j <= n;j++) {
            cin >> mp[i][j];
        }
    }
    floodfill(0, 0);
    for (int i = 1;i <= n;i++) {
        for (int j = 1;j <= n;j++) {
            cout << ((mp[i][j] == 1) ? 1 : ((mp[i][j] == 3) ? 0 : 2)) << " ";
        }
        cout << endl;
    }
    return 0;
}