🪄

泛洪算法:Photoshop 魔棒,点一下连成一片全变

魔棒点一个格子,和它四连通的同色格子全部变色——中间隔着别的颜色就渗不过去。本质就一句话:把网格当图(格子=节点、上下左右=边),从点击的格子做一次 BFS/DFS 遍历。这就是泛洪(flood fill)。

💡 打个比方

泛洪 = Photoshop 的魔棒工具:点一下一个格子,所有和它连成一片的同色格子全被选中——但「连成一片」要求上下左右挨着(四连通),斜着碰上的不算;中间隔着别的颜色(墙),水就渗不过去。把网格看成图:每个格子 = 一个节点,上下左右相邻的格子 = 一条边(8 向则连斜角也通),那么「魔棒」就是从点击的格子出发做一次图的 BFS(或 DFS),把能走到的格子全 visit 一遍。扩散时每到一个格子要问两件事:出界没?(行、列在不在 1..n、1..m 内,出界 = 掉下画布)和 去过了没?(已染色的格子水不再进)。问完都合格,才「染色 + 入队」。数岛屿、填湖、找连通块,全是这一招。

术语:网格图(格子 = 节点,上下左右 = 边)、4 向/8 向(四连通/八连通,斜角算不算相邻)、边界判断(1≤行≤n 且 1≤列≤m)、泛洪(flood fill,网格上的 BFS/DFS)、原地染色(直接把 0 改成 1 当「已访问」标记)、岛屿(连成一片的 1 块)。

🖼️ 大图看明白

泛洪:从 (1,1) 向上下左右渗水,被墙(1)挡住(蓝 = 已淹,灰 = 墙) 1 2 3 4 5 1 2 3 4 5 0 0 1 0 0 0 0 1 0 0 1 1 0 0 0 0 0 0 0 0 1 1 1 1 0 从 (1,1) 泛洪的队列推进 q: [(1,1)] → [(2,1),(1,2)] → [(1,2),(2,2)] → [(2,2)] → 空 共淹 4 格(蓝色): (1,1) (1,2) (2,1) (2,2) 水为什么到不了右半边? (2,3)=1、(3,2)=1 两堵墙把路堵死 规则:1 = 墙(灰),0 = 可淹(白→蓝) 蓝 = 已淹 灰 = 墙 白 = 还没淹到(属于右边那一大片,从 (1,1) 走不过去)

🧩 子任务 1:网格就是图——格子是节点,上下左右是边

🎯 小目标:把 n×m 网格「翻译」成图:会写方向数组(4 向/8 向),会写边界判断,会算一个格子最多有几个邻居。
// n 行 m 列的网格:格子 (r, c) = 节点;邻居 = 上下左右(4 向)
//
// 4 向方向数组(上、下、左、右):
//   int dr[4] = {-1, 1, 0, 0};
//   int dc[4] = { 0, 0,-1, 1};
//   邻居坐标:nr = r + dr[d],nc = c + dc[d](d = 0..3)
//
// 8 向:再加 4 个斜角(上下左右斜着走)
//   int dr8[8] = {-1,-1,-1, 0, 0, 1, 1, 1};
//   int dc8[8] = {-1, 0, 1,-1, 1,-1, 0, 1};
//
// 边界判断(出界 = 掉下画布):
//   bool ok(int r, int c) { return r >= 1 && r <= n && c >= 1 && c <= m; }
// 每访问一个格子:4 个方向都先问 ok(nr, nc),再问「去过/是墙没」
✅ 小检查:① 5×5 网格里,一个内部格子最多有几个 4 向邻居?角落格子呢?② 8 向比 4 向多了什么?什么题目该用 8 向?③ 边界判断 ok 的条件是什么?
看看答案 ① 内部格子 4 个;角落格子 2 个;边上非角落格子 3 个。 ② 多 4 个斜角邻居(共 8 个)。题目说「斜着也算相邻/连通/移动」(比如国际象棋的国王、某些地图题)才用 8 向;默认都是 4 向。 ③ 1 ≤ r ≤ n 且 1 ≤ c ≤ m(行、列都在网格内),写成 r>=1 && r<=n && c>=1 && c<=m。
⚠️ 易错:方向数组别写错顺序导致「斜角混进 4 向」——dr 和 dc 必须一一对应(第 d 个方向 = (dr[d], dc[d]))。8 向的 8 组配对要逐个数清楚,写错一个方向 = 漏一格或多淹一格。

🧩 子任务 2:手推 BFS 泛洪——5×5 网格从 (1,1) 淹多少格?

🎯 小目标:对下图(1 = 墙)从 (1,1) 手推 BFS 队列的每一步,数出被淹格子,并说清水到不了右半边的原因。
// 网格(1 = 墙,0 = 可淹),行 1..5、列 1..5:
//  0 0 1 0 0
//  0 0 1 0 0
//  1 1 0 0 0
//  0 0 0 0 0
//  1 1 1 1 0
//
// 从 (1,1) 做 BFS 泛洪(入队即标记为「已淹」):
// q = [(1,1)],已淹 {(1,1)}
// 出 (1,1):上/左出界;下 (2,1)=0 入队;右 (1,2)=0 入队  q=[(2,1),(1,2)]
// 出 (2,1):上(1,1)已淹;下(3,1)=1墙;左出界;右(2,2)=0 入队 q=[(1,2),(2,2)]
// 出 (1,2):上出界;下(2,2)已标记;左(1,1)已淹;右(1,3)=1墙   q=[(2,2)]
// 出 (2,2):上(1,2)已淹;下(3,2)=1墙;左(2,1)已淹;右(2,3)=1墙 q=[]
// 队空,结束。共淹 4 格:(1,1)、(1,2)、(2,1)、(2,2)
//
// 右半边那一大片(13 格)为什么淹不到?
//   (2,2) 往下的 (3,2) 是墙、往右的 (2,3) 也是墙——两条路全堵死。
//   (那片属于「另一座岛」,从 (1,1) 出发够不着。)
✅ 小检查:① 从 (1,1) 泛洪共淹几格?② 水到不了 (3,3) 的根本原因是什么?③ 代码里「已淹」标记在入队时做还是出队时做?为什么?
看看答案 ① 4 格:(1,1)、(1,2)、(2,1)、(2,2)。 ② (2,2) 的四个邻居里,通往右半边的两个方向都是墙((2,3)=1、(3,2)=1)——泛洪只能「渗」,穿不了墙。 ③ 入队时。若出队时才标,同一个格子可能从两个方向分别入队两次(重复排队)——和图 BFS 的「入队即标记」是同一个道理。
⚠️ 易错:泛洪前忘了先标记起点(起点也要「已淹」+ 入队);边界判断只写了一维(行判断了、列忘了)→ 越界访问数组(C++ 里就是随机读别人的内存,难查的 bug)。

🧩 子任务 3:数岛屿——泛洪当「涂色笔」,启动几次就是几座岛

🎯 小目标:掌握「外层扫描 + 遇新岛就泛洪涂色」的数岛屿套路,对下面的 4×5 网格手推出岛屿数。
// 4 行 5 列(1 = 岛,0 = 海):
//  1 1 0 0 0
//  1 0 0 0 1
//  0 0 0 1 1
//  0 0 0 0 0
//
// 套路:从第 1 行到第 4 行逐格扫描;
//   遇到「还没涂过的 1」→ 一定是一座新岛:cnt++,
//   从它出发泛洪,把整座岛涂成 2(涂色 = 「这座数过了」)。
//
// 手推:
//   (1,1) 是未涂 1 → 第 1 座岛,泛洪涂 {(1,1),(1,2),(2,1)} 为 2
//   (1,2) 已是 2,跳过;(1,3)(1,4)(1,5) 是 0,跳过
//   (2,1) 已是 2,跳过;(2,2)(2,3)(2,4) 是 0,跳过
//   (2,5) 是未涂 1 → 第 2 座岛,泛洪涂 {(2,5),(3,4),(3,5)} 为 2
//   剩下全是 0 或 2,扫描结束。cnt = 2
✅ 小检查:① 数过的岛为什么必须涂成 2?不涂会怎样?② 4×5 全 1 的网格有几座岛?③ 5×5 里只有 (1,1) 和 (2,2) 各一个 1、其余全 0:按 4 向数有几座?按 8 向呢?
看看答案 ① 防止重复数:不涂的话,外层 for 走到 (1,2)、(2,1) 时又看到 1,会误以为是新岛再 cnt++。涂色 = 「这座已经数过,路过跳过」。 ② 1 座(整张网格里上下左右全连成一片)。 ③ 4 向:2 座——(1,1) 和 (2,2) 行、列都差 1,4 向里算不相邻;8 向:1 座——斜角算相邻,连成一座。
⚠️ 易错:外层扫描「见 1 就 cnt++ + 泛洪涂色」,涂色一步不能省——省了的话,一座 3 格的岛会被数成 3(上面这张 4×5 会数出 6 而不是 2)。判断「是否未涂 1」要看当前数组的值,别拿涂色前的旧数据判断。

🧩 子任务 4:DFS 版 vs BFS 版——大网格选哪个;「去过」记在哪里

🎯 小目标:说清大网格为什么优先 BFS 泛洪;会选「改原数组」还是「另开 vis」;知道 int 时间戳能免去清表。
// 选法 1:DFS(递归)还是 BFS(队列)?
//   网格可能 1000 × 1000 = 100 万个格子。
//   递归 DFS:每进一格,调用栈上「压一层」;
//   一条长路径压 100 万层,系统栈(通常只有几 MB)不够用
//   → 「栈溢出」,程序直接崩。BFS 的队列开在自己管理的内存里,稳定。
//   所以泛洪默认写 BFS 版(除非题目保证网格很小)。
//
// 选法 2:「数过/淹过」的标记记在哪?
//   法 A:直接改原网格(1 → 2,或 0 → 1)——省内存,原数据没了
//   法 B:原网格不动,另开 bool vis[n][m]——多占一份内存,原数据留着
//   两者效果一样,按题目要求选。
//
// 小技巧:vis 用 int「编号」(时间戳),不每次都清表
//   int vis[1005][1005] = {0}; int id = 0;
//   每做一次泛洪:id++,访问的格子写 vis[r][c] = id;
//   判断「这次来过没」= vis[r][c] == id(不是 != 0!)
//   → 下一次泛洪不用花 O(nm) 清整张表,id++ 一下就好。
✅ 小检查:① 1000×1000 的网格为什么递归 DFS 有风险?② 题目要求「原网格必须保持不变」,标记记在哪?③ 时间戳法比「每次 memset 清表」省在哪里?④ 时间戳法判断「这次来过」应该写什么?
看看答案 ① 递归每格压一层调用栈,100 万格的长路径可能栈溢出;BFS 队列开在自己管理的内存里,大小可控。 ② 法 B:另开 vis 数组,原网格不动。 ③ 清整张表要 O(nm),做几十次泛洪就几十遍;时间戳 id++ 是 O(1),旧的编号留着不碍事。 ④ 必须写 vis[r][c] == id(等于「当前这次」的编号);写 != 0 会把上一次泛洪的旧编号也算成「来过」,漏掉格子。
⚠️ 易错:① 递归 DFS 用在大网格前,先估算「最长路径 ≈ 格子总数」会不会超栈;② 时间戳法每次泛洪开头必须 id++,判断必须用 ==id;③ 改原数组的版本里,起点也要先涂(和「入队即标记」一个道理)。

💻 完整代码:数岛屿 + 区域染色(一个 flood 两用)

🎯 小目标:读懂「一个泛洪函数 + 外层扫描」的完整程序;样例输入 4×5 网格 + (1,3),输出 2 和 14。
// 4 行 5 列:1 = 岛,0 = 海;先数岛屿,再染含 (1,3) 的海区
#include <bits/stdc++.h>
using namespace std;

int g[5][6];                       // g[1..4][1..5]
int dr[4] = {-1, 1, 0, 0};
int dc[4] = {0, 0, -1, 1};
bool ok(int r, int c) { return r >= 1 && r <= 4 && c >= 1 && c <= 5; }

// 泛洪(BFS):从 (sr,sc) 出发,把连通的 from 值整片改成 to,返回格子数
int flood(int sr, int sc, int from, int to) {
    queue<pair<int,int>> q;
    if (g[sr][sc] != from) return 0;
    g[sr][sc] = to;                // 起点先涂 + 入队
    q.push({sr, sc});
    int cnt = 0;
    while (!q.empty()) {
        int r = q.front().first, c = q.front().second;
        q.pop();
        cnt++;
        for (int d = 0; d < 4; d++) {
            int nr = r + dr[d], nc = c + dc[d];
            if (ok(nr, nc) && g[nr][nc] == from) {
                g[nr][nc] = to;    // 入队即涂色
                q.push({nr, nc});
            }
        }
    }
    return cnt;
}

int main() {
    for (int r = 1; r <= 4; r++)
        for (int c = 1; c <= 5; c++)
            cin >> g[r][c];

    // ① 数岛屿:逐格扫描,见到未涂的 1 就启动一次泛洪(1 → 2)
    int islands = 0;
    for (int r = 1; r <= 4; r++)
        for (int c = 1; c <= 5; c++)
            if (g[r][c] == 1) { islands++; flood(r, c, 1, 2); }
    cout << islands << endl;        // 样例 → 2

    // ② 区域染色:(1,3) 所在的海区有多大?(0 → 1)
    int sr, sc; cin >> sr >> sc;
    cout << flood(sr, sc, 0, 1) << endl;   // 样例 → 14
    return 0;
}

// 样例输入:
//   1 1 0 0 0
//   1 0 0 0 1
//   0 0 0 1 1
//   0 0 0 0 0
//   1 3
// 输出:2
//       14   ← 14 个 0 全连成一片,(1,3) 一次泛洪全染到

✋ 你来试试

第 1 题:大图那张 5×5 网格,把 (2,3) 的墙改成 0(其余墙不动),从 (1,1) 能淹多少格? 18 格。(2,3) 通了之后,水能渗进右半边:第 1 行 4 格((1,1)(1,2)(1,4)(1,5),(1,3) 还是墙)、第 2 行全 5 格、第 3 行 3 格、第 4 行全 5 格、第 5 行只有 (5,5) 1 格——共 4+5+3+5+1 = 18。原来那 4 格 + 新通的 14 格。
第 2 题:3×3 全 0 的网格有几座岛?3×3 全 1 呢? 全 0:0 座(全是海,一个 1 都没有);全 1:1 座(9 格上下左右全连通,一次泛洪染完)。
第 3 题:5×5 里只有 (1,1) 和 (2,2) 各一个 1。4 向数岛屿 = ?8 向 = ? 4 向 = 2 座(对角不算相邻);8 向 = 1 座(斜角相邻,一次泛洪染两格)。
第 4 题(加餐):1000×1000 的网格、一座超大连通块,你写递归 DFS 泛洪,可能出什么事?稳妥的写法是什么? 可能栈溢出:递归每格压一层,长路径接近 100 万层,系统栈装不下,程序崩溃。稳妥写法:① BFS + queue(队列开在自己管理的内存里);② 或手写栈(vector 当栈)模拟 DFS。小网格(≤50×50)递归才放心。
🎯 一句话记住:泛洪 = 魔棒——网格当图,每格只问两个问题「出界没?去过没?」;入队即标记;数岛屿 = 外层扫描 + 见 1 就 cnt++ 并泛洪涂色,启动几次就是几座;默认 4 向,题目说斜角也算相邻才换 8 向;大网格用 BFS(队列)防栈溢出,「去过」可以改原数组、另开 vis、或 int 时间戳(id++ 免清表)。