NOI C++ 课程体系 / 算法篇 / 第15讲
CSP-J 入门级 · 搜索算法 · m4-15

泛洪算法(Flood Fill)

像水一样蔓延,像火一样扩散 —— 掌握 Flood Fill 的核心思想,用 BFS/DFS 遍历连通区域,解决岛屿计数、填涂颜色、边界检测等经典问题!

6 大模块 2 种实现方式 3 个经典案例 5 大易错点 12+20 道练习题
本讲 · 第 39/56 讲泛洪算法 Flood Fill
模块 01

情景导航

从生活中的"蔓延"现象出发,直觉地理解 Flood Fill 的思想。

故事油漆泼洒的故事

小明在玩画图软件时,不小心把一桶蓝色油漆泼到了地板上。

油漆从泼洒点开始,向四周蔓延扩散——流到相邻的空地,再流到更远的空地……

第1步:油漆到达泼洒点 🟦
第2步:油漆流向上下左右四个相邻的空格 🟦🟦🟦
第3步:每个被染色的格子继续向四周扩散 🟦🟦🟦🟦🟦
直到:遇到墙壁(障碍物)或边界,油漆不再扩散 ⛔

最终,所有与泼洒点连通的空地都被染成了蓝色!

这个过程就是Flood Fill(泛洪算法)的核心思想:从一个起始点出发,沿着"可通行"的路径向四周扩展,直到填满整个连通区域。

Flood Fill = 从一点出发,沿连通路径蔓延,填满整个区域

💡 画图软件中的"油漆桶工具"

你在画图软件中用油漆桶点一下某个区域,整个连通区域就被填充了颜色——这就是 Flood Fill 最直观的应用!

• 点击位置 = 起始点

• 相同颜色的相邻像素 = 可通行路径

• 不同颜色的像素 = 障碍物(边界)

• 填充完成 = 整个连通区域被染色

关键问题:如何高效地遍历整个连通区域?用 BFS 还是 DFS?

①生活类比:蔓延无处不在
🔥

火灾蔓延

火从一处开始,沿可燃物扩散到相邻区域,直到遇到隔离带。

→ 连通区域遍历
💧

水灾扩散

洪水从决口处涌入,沿低洼地带蔓延,直到遇到高地。

→ 区域填充
🦠

传染病传播

病毒从感染者开始,传染给密切接触者,再传染给更多的人。

→ 连通分量
🎨

画图填充

点一下油漆桶,颜色从一个像素扩展到所有相邻同色像素。

→ 经典 Flood Fill
②编程定义

泛洪算法(Flood Fill):从一个起始节点出发,按照某种规则(如颜色相同、值相同、可通行等),遍历所有与起始点连通的节点。通常用于网格图或图的连通区域检测。

核心操作:标记已访问 + 向邻居扩展 + 递归/迭代,直到没有新的可达节点。

📌 本讲核心知识地图

板块核心内容对应大纲
Flood Fill核心从起点蔓延,标记+扩展泛洪算法
方向数组4方向 / 8方向遍历网格图遍历
岛屿计数遍历所有连通块并计数经典Flood Fill应用
填涂颜色反向思维 + 边界Flood Fill经典Flood Fill应用
最大连通块遍历每个连通块求面积经典Flood Fill应用
BFS vs DFS队列 vs 递归栈实现方式对比
③核心口诀
🔑 Flood Fill 三步走

① 标记:到达一个格子就立刻标记(防止重复访问)
② 扩展:向四个(或八个)方向检查邻居
③ 递归/入队:合法的未访问邻居继续蔓延

🔑 BFS vs DFS 选择

BFS(队列):逐层扩散,像水波纹,适合求最短距离;
DFS(递归):一条路走到黑再回头,代码简洁;
纯连通性问题两者皆可,求最短路选 BFS!

模块 02

知识探究

深入理解 Flood Fill 的核心思想、方向数组、BFS/DFS 两种实现方式,并通过三个经典案例掌握实战技巧。

1Flood Fill 核心思想 核心

Flood Fill 的本质是图的遍历(BFS 或 DFS)在网格图上的应用。关键在于:

🎯 连通性判断
  • 两个格子是否"相邻"
  • 两个格子是否"可通行"
  • 决定哪些格子属于同一区域
🚶 遍历策略
  • BFS:队列,逐层扩展
  • DFS:递归,深度优先
  • 两种方式都能遍历所有连通格
🔒 防止重复
  • 到达即标记 visited
  • 或将原地图值改掉
  • 否则会死循环!
🔑 Flood Fill 通用模板(DFS版)

void flood_fill(int x, int y) {
  if (越界 || 已访问 || 不可通行) return; // 边界+剪枝
  visited[x][y] = true; // 立刻标记!
  // 处理当前格子(计数/记录/染色...)
  for (int d = 0; d < 4; d++) // 四个方向
    flood_fill(x + dx[d], y + dy[d]);
}

2方向数组与连通方式 基础

在网格图中,从一个格子出发可以向不同方向扩展:

🧭 四方向(上下左右)

dx[] = {-1, 1, 0, 0}
dy[] = {0, 0, -1, 1}

  • 上下左右四个邻居
  • 最常用,大多数题用4方向
  • 对角线不算相邻
🧭 八方向(含对角线)

dx[] = {-1,-1,-1,0,0,1,1,1}
dy[] = {-1,0,1,-1,1,-1,0,1}

  • 加上四个对角邻居
  • 部分题目要求8方向连通
  • 如油田问题、扫雷
连通方式方向数适用场景示例
4方向4标准网格连通、迷宫岛屿计数、迷宫寻路
8方向8对角线也算连通油田、扫雷、填涂颜色
自定义按需骑士走日字等骑士最短路径
案例经典案例1:岛屿计数 必会

给定一个 m×n 的二维网格,'1' 表示陆地,'0' 表示水域。相连的陆地(上下左右)形成一个岛屿。求岛屿的数量。

算法思路:遍历每个格子,遇到未访问的 '1' → 发现新岛屿 → 用 Flood Fill 把整个岛屿标记为已访问 → 计数器 +1。

陆地(1)
水域(0)
已访问
当前

📖 示例网格(5×5)

1
1
0
0
1
1
0
0
0
1
0
0
1
1
0
0
0
1
0
0
1
1
0
0
1

遍历过程:

① 扫到 (0,0)='1' 且未访问 → 岛屿 #1 → Flood Fill 标记 (0,0)(0,1)(1,0) → 计数器 = 1

② 继续扫描,(0,4)='1' 且未访问 → 岛屿 #2 → Flood Fill 标记 (0,4)(1,4) → 计数器 = 2

③ 继续扫描,(2,2)='1' 且未访问 → 岛屿 #3 → Flood Fill 标记 (2,2)(2,3)(3,2) → 计数器 = 3

④ 继续扫描,(4,0)='1' 且未访问 → 岛屿 #4 → Flood Fill 标记 (4,0)(4,1) → 计数器 = 4

⑤ 继续扫描,(4,4)='1' 且未访问 → 岛屿 #5 → Flood Fill 标记 (4,4) → 计数器 = 5

✅ 共有 5 个岛屿

C++island_count.cpp
#include <iostream>
using namespace std;

int m, n;
char grid[105][105];
bool vis[105][105];
int dx[] = {-1, 1, 0, 0};  // 四方向
int dy[] = {0, 0, -1, 1};

void dfs(int x, int y) {
    vis[x][y] = true;  // 立刻标记
    for (int d = 0; d < 4; d++) {
        int nx = x + dx[d], ny = y + dy[d];
        if (nx >= 0 && nx < m && ny >= 0 && ny < n
            && !vis[nx][ny] && grid[nx][ny] == '1')
            dfs(nx, ny);  // 继续蔓延
    }
}

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

int cnt = 0;
    for (int i = 0; i < m; i++)
        for (int j = 0; j < n; j++)
            if (grid[i][j] == '1' && !vis[i][j]) {
                dfs(i, j);  // 发现新岛屿,Flood Fill
                cnt++;
            }
    cout << cnt << endl;
    return 0;
}
运行结果
输入:
5 5
1 1 0 0 1
1 0 0 0 1
0 0 1 1 0
0 0 1 0 0
1 1 0 0 1
输出:5

💡 为什么 Flood Fill 能正确计数?

每次找到一个未访问的 '1',说明发现了一个新的连通块(新岛屿)。

DFS/BFS 会把整个连通块内的所有格子都标记为已访问。

之后再扫到的 '1' 一定属于另一个连通块 → 不会重复计数。

时间复杂度:每个格子最多被访问一次 → O(m×n)

案例经典案例2:填涂颜色 经典

给定一个 n×n 的 0/1 矩阵,其中 1 围成的闭合区域内部的 0 需要被填充为 2。外部的 0 不变。

反向思维:不找"内部的0",而是找"外部的0"——从边界出发,Flood Fill 标记所有与边界连通的 0,剩下的 0 就是内部的。

📖 示例(6×6)

原始矩阵:

0
0
0
0
0
0
0
1
1
1
1
0
0
1
0
0
1
0
0
1
0
0
1
0
0
1
1
1
1
0
0
0
0
0
0
0

算法步骤:

① 在矩阵外加一圈虚拟边界,从 (0,0) 开始 Flood Fill,标记所有外部连通的 0

② 被标记的 0 = 外部(不变),未被标记的 0 = 内部(填为 2)

✅ 结果:(2,2)(2,3)(3,2)(3,3) 从 0 变为 2

C++flood_fill_paint.cpp — 反向思维
#include <iostream>
using namespace std;

int n, a[35][35];
bool vis[35][35];
int dx[] = {-1,1,0,0};
int dy[] = {0,0,-1,1};

void dfs(int x, int y) {
    vis[x][y] = true;
    for (int d = 0; d < 4; d++) {
        int nx = x + dx[d], ny = y + dy[d];
        // 注意:范围是 0 ~ n+1(含虚拟边界)
        if (nx >= 0 && nx <= n+1 && ny >= 0 && ny <= n+1
            && !vis[nx][ny] && a[nx][ny] == 0)
            dfs(nx, ny);
    }
}

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            cin >> a[i][j];
    // a[0][*], a[n+1][*], a[*][0], a[*][n+1] 默认为 0

dfs(0, 0);  // 从虚拟边界开始,标记所有外部 0

for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            if (a[i][j] == 0 && !vis[i][j])
                cout << 2 << " ";  // 内部 0 → 填 2
            else
                cout << a[i][j] << " ";
        }
        cout << endl;
    }
    return 0;
}
运行结果
输入:
6
0 0 0 0 0 0
0 1 1 1 1 0
0 1 0 0 1 0
0 1 0 0 1 0
0 1 1 1 1 0
0 0 0 0 0 0
输出:
0 0 0 0 0 0
0 1 1 1 1 0
0 1 2 2 1 0
0 1 2 2 1 0
0 1 1 1 1 0
0 0 0 0 0 0

💡 反向思维的妙处

直接找"被1包围的内部0"很难——你不知道哪些0是内部的。

但找"外部0"很简单:从边界出发 Flood Fill,能到达的 0 都是外部的。

剩下的 0 就是内部的!这就是补集思想在 Flood Fill 中的应用。

案例经典案例3:最大连通块面积 高频

给定一个 m×n 的网格,1 表示草地,0 表示水域。求最大的草地连通块面积(格子数)。

算法思路:对每个未访问的 '1' 做 Flood Fill,在 DFS 中累计访问格子数(面积),记录最大面积。

📖 示例(4×6)

1
1
0
0
1
1
1
1
0
1
1
0
0
0
1
1
1
0
0
0
1
1
0
0

遍历结果:

• 连通块1:(0,0)(0,1)(1,0)(1,1) → 面积 = 4

• 连通块2:(0,4)(0,5)(1,4)(2,3)(2,4)(3,2)(3,3)(2,2) → 面积 = 8

• 连通块3:(1,3) → 面积 = 1

✅ 最大连通块面积 = 8

C++largest_component.cpp
#include <iostream>
using namespace std;

int m, n, grid[105][105];
bool vis[105][105];
int dx[] = {-1,1,0,0};
int dy[] = {0,0,-1,1};
int area;  // 当前连通块面积

void dfs(int x, int y) {
    vis[x][y] = true;
    area++;  // 每访问一个格子,面积+1
    for (int d = 0; d < 4; d++) {
        int nx = x + dx[d], ny = y + dy[d];
        if (nx >= 0 && nx < m && ny >= 0 && ny < n
            && !vis[nx][ny] && grid[nx][ny] == 1)
            dfs(nx, ny);
    }
}

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

int maxArea = 0;
    for (int i = 0; i < m; i++)
        for (int j = 0; j < n; j++)
            if (grid[i][j] == 1 && !vis[i][j]) {
                area = 0;       // 重置面积
                dfs(i, j);       // Flood Fill
                maxArea = max(maxArea, area);
            }
    cout << maxArea << endl;
    return 0;
}
运行结果
输入:
4 6
1 1 0 0 1 1
1 1 0 1 1 0
0 0 1 1 1 0
0 0 1 1 0 0
输出:8
⚡Flood Fill 的常见变体 拓展
🔄 BFS 实现 Flood Fill
  • 用队列替代递归
  • 入队时立刻标记(防止重复入队)
  • 不会栈溢出,适合大图
  • 可以同时记录层数(距离)
queue<pair<int,int>> q;
q.push({sx, sy});
vis[sx][sy] = true;
while (!q.empty()) {
  auto [x,y] = q.front();
  q.pop();
  for (int d=0;d<4;d++){
    int nx=x+dx[d],ny=y+dy[d];
    if(合法&&!vis[nx][ny]){
      vis[nx][ny]=true;
      q.push({nx,ny});
    }
  }
}
🎯 常见应用模式
  • 连通块计数:扫全图+Flood Fill
  • 连通块面积:DFS中累计计数
  • 边界检测:从边界Flood Fill标记外部
  • 最短距离:BFS逐层扩散
  • 区域染色:给不同连通块标不同编号
  • 路径存在性:Flood Fill看能否到达终点
实现方式数据结构优点缺点适用场景
DFS递归调用栈代码简洁大网格可能栈溢出网格较小(≤1000×1000)
BFS队列 queue不爆栈,可记距离代码稍长大网格、需要最短路
模块 03

实践应用

通过详细的分析和编程题,巩固 Flood Fill 的实战能力。

题1Flood Fill 过程追踪

题目:对以下 4×4 网格,从 (0,0) 开始做 4 方向 Flood Fill(值为 0 可通行),逐步追踪访问顺序。

0
0
1
0
0
0
1
0
1
0
0
0
0
1
0
0
查看参考答案

DFS 访问顺序追踪(递归版):

dfs(0,0): 标记 → 方向顺序 上(越界) 下(1,0)✓ 左(越界) 右(0,1)✓

  → 先访问 dfs(1,0)

    dfs(1,0): 标记 → 上(0,0已访问) 下(2,0)=1不通 左越界 右(1,1)✓

      → dfs(1,1): 标记 → 上(0,1)✓ 下(2,1)✓ 左(1,0已访问) 右(1,2)=1不通

        → dfs(0,1): 标记 → 上越界 下(1,1已访问) 左(0,0已访问) 右(0,2)=1不通

        → 返回,继续 dfs(2,1): 标记 → 上(1,1已访问) 下(3,1)=1不通 左(2,0)=1不通 右(2,2)✓

          → dfs(2,2): 标记 → 上(1,2)=1不通 下(3,2)✓ 左(2,1已访问) 右(2,3)✓

            → dfs(3,2): 标记 → 上(2,2已访问) 下越界 左(3,1)=1不通 右(3,3)✓

              → dfs(3,3): 标记 → 上(2,3)✓ ...

                → dfs(2,3): 标记 → 上(1,3)✓ ...

                  → dfs(1,3): 标记 → 上(0,3)✓

                    → dfs(0,3): 标记 → 全部邻居已访问或不通

访问顺序:(0,0)→(1,0)→(1,1)→(0,1)→(2,1)→(2,2)→(3,2)→(3,3)→(2,3)→(1,3)→(0,3)

共访问 11 个格子(所有与 (0,0) 连通的 0 都被访问到了)

注意:(0,3) 虽然被墙隔开,但通过下方的路径仍然可达!

题2八方向连通块

题目:给定 5×5 网格,1 表示油田,0 表示空地。对角线相邻的 1 也算同一个油田(8方向连通)。求油田数量。

1
0
0
1
0
0
1
0
0
1
0
0
1
0
0
1
0
0
1
0
0
1
0
0
1
查看参考答案

关键区别:使用 8 方向数组

dx[] = {-1,-1,-1,0,0,1,1,1}; dy[] = {-1,0,1,-1,1,-1,0,1};

分析 8 方向连通性:

• (0,0) 的 8 邻居包含 (1,1),所以 (0,0) 和 (1,1) 连通

• (1,1) 的 8 邻居包含 (2,2),所以 (1,1) 和 (2,2) 连通

• 主对角线:(0,0)↗(1,1)↗(2,2)↗(3,3)↗(4,4),构成第 1 个油田(5 格)

• 右上角:(0,3)↗(1,4) 8 方向相邻,构成第 2 个油田(2 格)。注意 (1,4) 与 (2,2) 列差为 2,并不相邻!

• 左下角:(3,0)↘(4,1) 8 方向相邻,构成第 3 个油田(2 格)

答案:3 个油田(如果是 4 方向,则有 9 个独立油田)

题3边界着色问题

题目:给定 m×n 网格和起始点 (r,c),将包含 (r,c) 的连通块的边界格子着为指定颜色 color。边界格子定义为:属于该连通块,且至少有一个 4 方向邻居不属于该连通块(或是网格边界)。

提示:先 Flood Fill 找出整个连通块,再判断每个格子是否为边界。
查看参考答案

算法步骤:

① 从 (r,c) 开始 Flood Fill,标记连通块中所有格子(颜色 == grid[r][c])

② 遍历连通块中的每个格子,检查其 4 个邻居:

  - 如果某个邻居越界或颜色不同 → 当前格子是边界

③ 将边界格子着为 color

#include <iostream>
#include <vector>
using namespace std;
int m, n, grid[55][55];
bool vis[55][55];
int dx[] = {-1,1,0,0};
int dy[] = {0,0,-1,1};
vector<pair<int,int>> comp;  // 存储连通块
int origColor;
void dfs(int x, int y) {
    vis[x][y] = true;
    comp.push_back({x, y});
    for (int d = 0; d < 4; d++) {
        int nx = x+dx[d], ny = y+dy[d];
        if(nx>=0&&nx<m&&ny>=0&&ny<n
           &&!vis[nx][ny]&&grid[nx][ny]==origColor)
            dfs(nx, ny);
    }
}
int main() {
    int r, c, color;
    cin >> m >> n >> r >> c >> color;
    for(int i=0;i<m;i++)
        for(int j=0;j<n;j++) cin >> grid[i][j];
    origColor = grid[r][c];
    dfs(r, c);
    // 判断每个格子是否为边界
    for(auto [x,y] : comp) {
        bool isBorder = false;
        for(int d=0;d<4;d++) {
            int nx=x+dx[d], ny=y+dy[d];
            if(nx<0||nx>=m||ny<0||ny>=n
               ||grid[nx][ny]!=origColor)
                isBorder = true;
        }
        if(isBorder) grid[x][y] = color;
    }
    for(int i=0;i<m;i++){
        for(int j=0;j<n;j++) cout << grid[i][j] << " ";
        cout << endl;
    }
    return 0;
}

关键:用 vector 存储连通块中的所有格子,遍历完后再统一着色,避免 DFS 过程中修改颜色影响后续判断。

模块 04

总结提升

1核心要点总结
知识板块必须记住的核心
Flood Fill 核心从起点出发,标记+扩展,遍历整个连通区域
方向数组4方向:dx[]={−1,1,0,0}, dy[]={0,0,−1,1}
DFS 实现递归调用,代码简洁,注意栈深度
BFS 实现队列实现,入队即标记,不会栈溢出
岛屿计数扫全图 + 遇未访问则 Flood Fill + 计数
填涂颜色反向思维:从边界 Flood Fill 标记外部
最大连通块DFS 中累加面积,取最大值
时间复杂度每格最多访问一次 → O(m×n)
防重复关键到达即标记 visited,绝对不能忘!
2拓展:多源 Flood Fill

💡 多源 BFS — 从多个起点同时扩散

有时候题目要求从多个起点同时开始 Flood Fill,例如:

• "求每个格子到最近陆地的距离" → 所有陆地同时入队

• "火灾从多个着火点蔓延" → 所有着火点同时入队

方法:初始化时把所有源点都放入队列,然后正常 BFS。每个格子第一次被访问时的层数就是到最近源点的距离。

时间复杂度:仍然是 O(m×n),因为每个格子只入队一次。

典型例题:LeetCode 1162 "地图分析",P1162 "填涂颜色" 的变体。

3思考与启发

💡 学完本讲,你应该掌握的6项能力

1. 能用 Flood Fill 遍历网格图中的连通区域。

2. 能灵活使用 4方向和8方向 数组。

3. 能用 Flood Fill 实现连通块计数和面积统计。

4. 掌握反向思维:用 Flood Fill 标记外部来间接找内部。

5. 能说出 BFS 和 DFS 实现 Flood Fill 的区别和选择。

6. 了解多源 BFS 的应用场景。

模块 05

学习检测

5 个初学者最常犯的错误。

易错1

忘记标记 visited

❌ 错误
进入 dfs 后不标记,直接递归邻居 → A 访问 B,B 又访问 A,死循环!
✅ 正确
进入 dfs 的第一行就标记 visited[x][y]=true。或用 grid[x][y]=0 原地修改。
口诀:进门先打卡,vis=true 不能忘!
易错2

方向数组写错

❌ 错误
dx 和 dy 不对应,或漏掉某个方向,导致某些邻居没被遍历到。
✅ 正确
严格对应:dx[]={-1,1,0,0}; dy[]={0,0,-1,1}; 上下左右一一对应。
口诀:dx dy 要对齐,上下左右不遗漏!
易错3

边界条件没判全

❌ 错误
只判断了 x >= 0 和 y >= 0,忘了判断 x < m 和 y < n → 数组越界!
✅ 正确
四个条件缺一不可:nx>=0 && nx<m && ny>=0 && ny<n
口诀:边界判断四个全,大于等于零小于n!
易错4

BFS 入队不标记

❌ 错误
把格子放入队列后再标记 → 同一个格子可能被多次入队,效率暴跌甚至 MLE。
✅ 正确
入队的同时标记 vis[nx][ny]=true,确保每个格子只入队一次。
口诀:BFS 入队即打卡,先标后推不会乱!
易错5

连通块计数放错位置

❌ 错误
在 dfs 内部 cnt++ → 统计的是格子数,不是连通块数。
✅ 正确
cnt++ 放在主循环中(发现新连通块时),dfs 内部统计面积。
口诀:计数在外层,面积在内部!
速查Flood Fill 速查表
问题类型算法思路方向实现
连通块计数扫全图+Flood Fill+外层cnt++4方向DFS
连通块面积DFS中area++,取max4方向DFS
填涂颜色反向:边界Flood Fill标记外部4方向DFS/BFS
最短距离BFS逐层扩散,记录层数4/8方向BFS
区域染色Flood Fill时给编号4方向DFS
多源扩散所有源点同时入队BFS4方向BFS
📌知识点笔记卡

🌊 Flood Fill 核心

从起点蔓延扩散

标记+扩展+递归

遍历整个连通区域

🧭 方向数组

4方向:上下左右

8方向:含对角线

dx/dy 严格对齐

🏝️ 岛屿计数

扫全图找未访问1

Flood Fill标整个岛

外层 cnt++

🎨 填涂颜色

反向思维找外部0

从边界 Flood Fill

剩下的0是内部

📐 最大面积

DFS 中 area++

每块结束后取 max

O(m×n) 总复杂度

⚠️ 防坑指南

进门先标记 visited

边界判断四个全

BFS 入队即打卡

课后练习

分层练习作业

A基础 / B提高 / C挑战,难度递增,点击展开答案与解析。

🟢 A组 · 基础(4题)

A1选择题

Flood Fill 算法的核心操作是()

A. 对所有元素排序
B. 从起点出发,沿连通路径遍历整个区域
C. 用二分法查找目标
D. 枚举所有可能的路径

查看参考答案

答案:B

Flood Fill 的核心是"从一个点出发,像水一样蔓延,遍历所有连通的格子"。A是排序,C是二分,D是枚举,都不对。

考点:Flood Fill 的核心是从一个起点出发、像水一样向连通的邻居不断蔓延,本质是遍历一个连通块。

易错:把 Flood Fill 与“求最短路”混为一谈——单纯的扩散不保证最短,必须用 BFS 分层记录距离才能求步数。

A2选择题

在 Flood Fill 中,如果忘记标记 visited,最可能的后果是()

A. 结果偏大
B. 死循环(栈溢出或无限循环)
C. 结果偏小
D. 没有影响

查看参考答案

答案:B. 死循环

A 访问 B,B 又访问 A,A 再访问 B... 无限循环。DFS 会导致栈溢出(Runtime Error),BFS 会导致队列无限增长(Memory Limit Exceeded)。

考点:visited 标记让搜索有界,缺少它会在相邻格子之间来回跳,永远无法收敛。

易错:只在 DFS 里标记而 BFS 实现里忘记标记(或反之),同样会出现无限循环。

A3填空题

四方向的 dx 数组为 {-1, 1, 0, 0},对应的 dy 数组应为 ____。

查看参考答案
C++完整参考代码
#include <iostream>
using namespace std;

int n = 5, m = 5;
char g[5][5] = {
    {'1', '1', '0', '0', '0'},
    {'1', '0', '0', '1', '1'},
    {'0', '0', '0', '1', '0'},
    {'1', '1', '0', '0', '1'},
    {'1', '1', '0', '0', '1'}
};

// 四方向偏移:必须一一对应
int dx[4] = {-1,  1, 0,  0};   // 上 下 左  右  (行)
int dy[4] = { 0,  0, -1, 1};   // 上 下 左  右  (列)

void dfs(int x, int y) {
    g[x][y] = '0';                        // 标记已访问,避免重复走
    for (int i = 0; i < 4; i++) {         // 枚举 4 个方向
        int nx = x + dx[i], ny = y + dy[i];
        if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;  // 越界判断
        if (g[nx][ny] == '1') dfs(nx, ny);                     // 继续泛洪
    }
}

int main() {
    int cnt = 0;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            if (g[i][j] == '1') {         // 发现新连通块
                cnt++;
                dfs(i, j);                // 把整块泛洪掉
            }
    cout << "连通块数量 = " << cnt << endl;   // 4
    return 0;
}

答案:dy 数组应为 {0, 0, -1, 1}。对应关系:(-1, 0) = 上、(1, 0) = 下、(0, -1) = 左、(0, 1) = 右。dx 与 dy 必须严格一一对应,顺序错一个方向,搜索就会跑到错误的位置。

思路:把 dx 想成「行怎么变」、dy 想成「列怎么变」。往上走行号减 1、列不变,所以配 (0);往下走行号加 1、列不变,也配 (0);往左走列号减 1,配 (-1);往右走列号加 1,配 (1)。把它们按同样顺序写成两个数组,之后一个 for 循环 i 从 0 到 3,就能同时枚举四个方向,代码比写四段 if 清爽得多。

考点:方向数组(dx/dy)的构造与一一对应;泛洪算法 Flood Fill 的 DFS 递归写法;边界判断 nx/ny 的取值范围;网格连通块的计数。

易错:① dx 与 dy 顺序对不上(比如把 {0,0,-1,1} 写成 {0,-1,0,1}),方向就乱了;② 忘记边界判断,递归越界访问崩溃;③ 不标记已访问(g[x][y]='0'),会在两个格子间来回递归导致栈溢出;④ 边界条件写成 nx <= n 或 nx < 1,多搜或少搜一行/一列;⑤ 行列搞混——本题约定第一个下标是行 x,与 dy 配的是列坐标 y。

A4编程题

输入 m×n 的 0/1 矩阵,求岛屿(值为1的4方向连通块)的数量。

查看参考答案
#include <iostream>
using namespace std;
int m,n,g[105][105];
bool v[105][105];
int dx[]={-1,1,0,0}, dy[]={0,0,-1,1};
void dfs(int x,int y){
    v[x][y]=true;
    for(int d=0;d<4;d++){
        int nx=x+dx[d],ny=y+dy[d];
        if(nx>=0&&nx<m&&ny>=0&&ny<n&&!v[nx][ny]&&g[nx][ny]==1)
            dfs(nx,ny);
    }
}
int main(){
    cin>>m>>n;
    for(int i=0;i<m;i++)for(int j=0;j<n;j++)cin>>g[i][j];
    int cnt=0;
    for(int i=0;i<m;i++)for(int j=0;j<n;j++)
        if(g[i][j]==1&&!v[i][j]){dfs(i,j);cnt++;}
    cout<<cnt<<endl;
}

思路:遍历每个格子,遇到值为 1 且未被访问的就启动一次 DFS 把整座岛屿标记掉,计数器加一。

易错:DFS 里漏掉边界判断导致数组越界;或标记时机太晚,同一座岛屿被重复计数。

🟡 B组 · 提高(4题)

B1编程题

给定 n×n 矩阵,用 Flood Fill 求出所有连通块中面积最大的那个的面积(4方向连通,值为1的格子)。

查看参考答案

思路:在 DFS 中用全局变量 area 累计当前连通块面积,每次 Flood Fill 结束后用 maxArea = max(maxArea, area) 更新。

#include <iostream>
using namespace std;
int n,g[105][105],area,maxA;
bool v[105][105];
int dx[]={-1,1,0,0},dy[]={0,0,-1,1};
void dfs(int x,int y){
    v[x][y]=true; area++;
    for(int d=0;d<4;d++){
        int nx=x+dx[d],ny=y+dy[d];
        if(nx>=0&&nx<n&&ny>=0&&ny<n&&!v[nx][ny]&&g[nx][ny]==1)
            dfs(nx,ny);
    }
}
int main(){
    cin>>n;
    for(int i=0;i<n;i++)for(int j=0;j<n;j++)cin>>g[i][j];
    for(int i=0;i<n;i++)for(int j=0;j<n;j++)
        if(g[i][j]==1&&!v[i][j]){
            area=0; dfs(i,j);
            maxA=max(maxA,area);
        }
    cout<<maxA<<endl;
}

易错:area 作为全局变量时忘记在每块连通块开始泛洪前清零,面积会一直累加,结果越来越大。

B2编程题

P1162 填涂颜色:n×n 矩阵中 1 围成的闭合区域内部的 0 改为 2,外部 0 不变。

查看参考答案

反向思维:从虚拟边界 (0,0) 开始 Flood Fill,标记所有外部 0。剩余未标记的 0 就是内部的。

关键:数组大小开到 (n+2)×(n+2),第 0 行/列和第 n+1 行/列为虚拟边界(全 0)。

#include <iostream>
using namespace std;
int n,a[35][35];
bool vis[35][35];
int dx[]={-1,1,0,0},dy[]={0,0,-1,1};
void dfs(int x,int y){
    vis[x][y]=true;
    for(int d=0;d<4;d++){
        int nx=x+dx[d],ny=y+dy[d];
        if(nx>=0&&nx<=n+1&&ny>=0&&ny<=n+1&&!vis[nx][ny]&&a[nx][ny]==0)
            dfs(nx,ny);
    }
}
int main(){
    cin>>n;
    for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)cin>>a[i][j];
    dfs(0,0);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            if(a[i][j]==0&&!vis[i][j])cout<<2<<' ';
            else cout<<a[i][j]<<' ';
        }
        cout<<endl;
    }
}

思路:在矩阵外再加一圈全 0 的虚拟边界,从 (0,0) 泛洪标记所有外部 0,剩下未被标记的 0 就是封闭区域内部。

易错:数组只开 n×n 而没有向外扩一圈,紧贴边界的内部区域无法与外部分开,判断结果出错。

B3编程题

给定 m×n 网格和起点 (r,c),用 BFS 实现 Flood Fill,输出所有可达格子的坐标。

查看参考答案
#include <iostream>
#include <queue>
using namespace std;
int m,n,g[105][105],r,c;
bool vis[105][105];
int dx[]={-1,1,0,0},dy[]={0,0,-1,1};
int main(){
    cin>>m>>n>>r>>c;
    for(int i=0;i<m;i++)for(int j=0;j<n;j++)cin>>g[i][j];
    queue<pair<int,int>> q;
    q.push({r,c}); vis[r][c]=true;
    while(!q.empty()){
        auto [x,y]=q.front(); q.pop();
        cout<<'('<<x<<','<<y<<") ";
        for(int d=0;d<4;d++){
            int nx=x+dx[d],ny=y+dy[d];
            if(nx>=0&&nx<m&&ny>=0&&ny<n&&!vis[nx][ny]&&g[nx][ny]==g[r][c]){
                vis[nx][ny]=true;
                q.push({nx,ny});
            }
        }
    }
    cout<<endl;
}

注意:BFS 中 vis 在入队时设置(不是在出队时),防止重复入队。

思路:用队列保存待扩展的格子,每次出队时输出坐标,并把四个方向上的可达格子入队并立即标记。

B4分析题

对一个 1000×1000 的全 0 网格做 Flood Fill(从 (0,0) 出发),用 DFS 实现可能会遇到什么问题?如何解决?

查看参考答案

问题:栈溢出(Stack Overflow)

DFS 递归深度最大可达 1000×1000 = 1,000,000 层,远超系统栈限制(通常 8MB ≈ 几十万层)。

解决方案:

① 改用 BFS(队列实现),不受栈深度限制

② 手动增大栈空间(编译选项或 #pragma)

③ 用显式栈模拟递归(手动管理栈)

竞赛推荐:大图一律用 BFS 实现 Flood Fill。

考点:递归 DFS 的深度受系统栈大小限制,超大网格需要改用 BFS 或显式栈来避免栈溢出。

易错:以为“数组开大一点就够了”——限制来自递归深度而不是数组大小;也并非换成非递归写法就一定更省内存。

🔴 C组 · 挑战(4题)

C1综合编程

给定 n×n 网格,0 表示空地,1 表示障碍物。求从 (0,0) 到 (n-1,n-1) 是否存在一条路径(4方向,只走 0)。如果存在,输出最短步数;否则输出 -1。

查看参考答案

思路:这是 Flood Fill + 最短路问题。用 BFS 从 (0,0) 出发,逐层扩散,记录层数(距离)。

第一次到达 (n-1,n-1) 时的层数就是最短距离。

#include <iostream>
#include <queue>
using namespace std;
int n,g[105][105],dist[105][105];
int dx[]={-1,1,0,0},dy[]={0,0,-1,1};
int main(){
    cin>>n;
    for(int i=0;i<n;i++)for(int j=0;j<n;j++)cin>>g[i][j];
    if(g[0][0]==1||g[n-1][n-1]==1){cout<<-1; return 0;}
    for(int i=0;i<n;i++)for(int j=0;j<n;j++)dist[i][j]=-1;
    queue<pair<int,int>> q;
    q.push({0,0}); dist[0][0]=0;
    while(!q.empty()){
        auto[x,y]=q.front(); q.pop();
        if(x==n-1&&y==n-1){cout<<dist[x][y]; return 0;}
        for(int d=0;d<4;d++){
            int nx=x+dx[d],ny=y+dy[d];
            if(nx>=0&&nx<n&&ny>=0&&ny<n&&g[nx][ny]==0&&dist[nx][ny]==-1){
                dist[nx][ny]=dist[x][y]+1;
                q.push({nx,ny});
            }
        }
    }
    cout<<-1;  // 不可达
}

易错:只用 DFS 判断连通性而不按层次记录距离,得到的步数不是最短步数(应用 BFS)。

C2综合编程

给定 m×n 的字符矩阵,'*' 表示油田,'.' 表示空地。对角线相邻的油田也算同一块(8方向连通)。求油田块数。

查看参考答案

关键区别:使用 8 方向数组。

#include <iostream>
using namespace std;
int m,n;
char g[105][105];
bool v[105][105];
int dx[]={-1,-1,-1,0,0,1,1,1};
int dy[]={-1,0,1,-1,1,-1,0,1};
void dfs(int x,int y){
    v[x][y]=true;
    for(int d=0;d<8;d++){
        int nx=x+dx[d],ny=y+dy[d];
        if(nx>=0&&nx<m&&ny>=0&&ny<n&&!v[nx][ny]&&g[nx][ny]=='*')
            dfs(nx,ny);
    }
}
int main(){
    cin>>m>>n;
    for(int i=0;i<m;i++)for(int j=0;j<n;j++)cin>>g[i][j];
    int cnt=0;
    for(int i=0;i<m;i++)for(int j=0;j<n;j++)
        if(g[i][j]=='*'&&!v[i][j]){dfs(i,j);cnt++;}
    cout<<cnt<<endl;
}

这就是经典的"油田问题"(UVa 572),8方向连通是关键。

思路:与 4 方向版本唯一的区别是把方向数组扩充到 8 个方向,其余泛洪逻辑完全一致。

易错:方向数组只写了 4 个(漏掉对角方向);或 (dx,dy) 配对写错,把方向搞乱。

C3综合编程

给定 n×m 网格(0=水,1=陆地),先求岛屿数量,再求最大岛屿面积,最后将所有岛屿编号(不同岛屿标不同数字)。

查看参考答案

思路:Flood Fill 时给连通块编号(id),同时统计面积。

#include <iostream>
using namespace std;
int n,m,g[105][105],id_map[105][105],area,maxA,cnt;
int dx[]={-1,1,0,0},dy[]={0,0,-1,1};
void dfs(int x,int y,int id){
    id_map[x][y]=id; area++;
    for(int d=0;d<4;d++){
        int nx=x+dx[d],ny=y+dy[d];
        if(nx>=0&&nx<n&&ny>=0&&ny<m&&g[nx][ny]==1&&id_map[nx][ny]==0)
            dfs(nx,ny,id);
    }
}
int main(){
    cin>>n>>m;
    for(int i=0;i<n;i++)for(int j=0;j<m;j++)cin>>g[i][j];
    for(int i=0;i<n;i++)for(int j=0;j<m;j++)
        if(g[i][j]==1&&id_map[i][j]==0){
            cnt++; area=0;
            dfs(i,j,cnt);
            maxA=max(maxA,area);
        }
    cout<<"岛屿数: "<<cnt<<endl;
    cout<<"最大面积: "<<maxA<<endl;
    for(int i=0;i<n;i++){
        for(int j=0;j<m;j++)cout<<id_map[i][j]<<' ';
        cout<<endl;
    }
}

易错:一次泛洪结束后忘记累加岛屿数或更新最大面积;或编号 id 从 0 开始,与“未访问”的默认值撞在一起。

C4综合编程

给定 n×m 网格,多个火灾源点同时起火,每秒向 4 方向蔓延一格。求每个格子被烧到的最早时间。不可达的格子输出 -1。

查看参考答案

多源 BFS:所有火源同时入队,dist 初始为 -1(未烧到),火源 dist=0。

#include <iostream>
#include <queue>
using namespace std;
int n,m,g[105][105],dist[105][105];
int dx[]={-1,1,0,0},dy[]={0,0,-1,1};
int main(){
    cin>>n>>m;
    queue<pair<int,int>> q;
    for(int i=0;i<n;i++)for(int j=0;j<m;j++){
        cin>>g[i][j]; dist[i][j]=-1;
        if(g[i][j]==1){q.push({i,j}); dist[i][j]=0;}
    }
    while(!q.empty()){
        auto[x,y]=q.front(); q.pop();
        for(int d=0;d<4;d++){
            int nx=x+dx[d],ny=y+dy[d];
            if(nx>=0&&nx<n&&ny>=0&&ny<m&&dist[nx][ny]==-1){
                dist[nx][ny]=dist[x][y]+1;
                q.push({nx,ny});
            }
        }
    }
    for(int i=0;i<n;i++){
        for(int j=0;j<m;j++)cout<<dist[i][j]<<' ';
        cout<<endl;
    }
}

关键:多源 BFS 的正确性在于——每个格子第一次被访问时就是到最近火源的最短距离。

思路:把所有火源一次性全部入队并令其 dist=0,只需一次多源 BFS 就能得到每个格子被烧到的最早时间。

易错:对每个火源分别做 BFS 再取最小值,复杂度爆炸还容易算错;或 dist 没初始化为 −1,未烧到的格子被输出成 0。

🏆 洛谷原题精选(20道)

以下题目均与 Flood Fill / 连通块 / 网格遍历相关,按难度递进排列。建议在洛谷平台提交练习。

1P1162填涂颜色入门
2P1605迷宫入门
3P1036选数入门
4P1025数的划分入门
5P2404自然数的拆分问题入门
6P1219八皇后入门
7P1101单词方阵入门
8P114101迷宫普及
9P1433吃奶酪普及
10P1434滑雪普及
11P1019单词接龙普及
12P1040加分二叉树普及
13P1514引水入城普及
14P1331海战游戏普及
15P1092虫食算普及
16P1074靶形数独提高
17P1120小木棍提高
18P1312Mayan游戏提高
19P1034矩形覆盖提高
20P1506拯救oibh总部普及
题号核心考点建议方法
P1162反向 Flood Fill 标记外部DFS/BFS 从边界出发
P1605迷宫路径搜索DFS + 回溯
P1141连通块大小查询Flood Fill + 记忆化
P1434网格最长路径(记忆化搜索)DFS + 记忆化
P1514多源 Flood Fill + 贪心BFS 从边界出发
P13318方向连通块 + 合法性检查Flood Fill 8方向
P1506反向 Flood Fill(类似填涂颜色)从边界 Flood Fill