在算法世界里,有一类问题总是让新手头疼 ——“找连通块”。比如 “田里有多少个水坑”“地图上有多少片森林”“如何标记被包围的区域”,这些问题本质上都属于Floodfill 问题。今天,我们就把 Floodfill 的底层逻辑、实现方法(DFS/BFS)、经典例题掰开揉碎讲清楚,让你不仅会写代码,更能理解 “为什么这么写”,从此连通块问题再也不是难题!下面就让我们正式开始吧!
Floodfill,直译是 “洪水填充”,就像现实中洪水蔓延一样:从某个 “起点” 出发,把所有和起点 “连通” 的区域都 “染成同一种颜色”,最终通过这种方式找到所有连通块,或者标记特定区域。
举个生活中的例子:你有一张画满格子的纸,有些格子画了 “水”(W),有些是 “旱地”(.)。现在你往一个 “水格子” 里倒墨水,墨水会扩散到所有相邻的 “水格子”(包括上下左右、斜对角),最后被墨水染过的格子就是一个 “水坑”—— 这就是 Floodfill 的核心逻辑:找到与起点连通的所有节点,并进行标记或统计。
所有 Floodfill 问题,本质上都围绕两个问题展开:
Floodfill 的实现完全依赖搜索算法,最常用的两种方式是DFS(深度优先搜索) 和BFS(宽度优先搜索)。两者各有特点,之前的博客中已经为大家介绍过了,我们就通过一个简单对比,让大家快速回顾一下它们的差异:
实现方式 | 核心工具 | 特点 | 适用场景 |
|---|---|---|---|
DFS | 递归 / 栈 | “一条路走到黑”,代码简洁,容易实现 | 小规模网格,担心队列内存时 |
BFS | 队列 | “逐层扩散”,避免递归栈溢出,逻辑直观 | 大规模网格,需控制搜索顺序时 |
接下来,我们会通过具体例题,分别演示两种实现方式的用法,你可以根据实际场景选择更合适的方案。
光说不练假把式,接下来我们结合两道经典例题(洛谷 P1596 Lake Counting、洛谷 P1162 填涂颜色),从 “统计连通块数量” 到 “标记特定连通区域”,一步步带你掌握 Floodfill 的核心技巧。
题目链接:https://www.luogu.com.cn/problem/P1596

农民约翰的田地被暴雨淹没,形成了多个水坑。田地用 N×M 的网格表示,每个格子是 “W”(水)或 “.”(旱地)。一个格子与周围 8 个格子(上下左右 + 斜对角)连通的 “水格子” 视为一个水坑。请统计田地中水坑的数量。
这是 Floodfill 的入门题,核心需求是 “统计连通块数量”。解题思路非常清晰:
DFS 的核心是 “递归遍历”:从起点出发,递归访问所有连通的 “W”,并标记为已访问。
#include <iostream>
#include <cstring>
using namespace std;
const int N = 110; // 题目中N、M≤100,开110足够
char g[N][N]; // 存储田地网格
bool st[N][N]; // 标记是否已访问
int n, m;
// 8个方向的方向数组(上下左右+斜对角)
int dx[] = {0, 0, 1, -1, 1, 1, -1, -1};
int dy[] = {1, -1, 0, 0, 1, -1, 1, -1};
// DFS:从(x,y)出发,标记所有连通的W
void dfs(int x, int y) {
st[x][y] = true; // 标记当前格子已访问
// 遍历8个方向
for (int k = 0; k < 8; k++) {
int nx = x + dx[k]; // 新格子的x坐标
int ny = y + dy[k]; // 新格子的y坐标
// 检查边界:nx在1~n,ny在1~m(注意:网格从1开始更直观)
// 检查是否未访问且是水(W)
if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && !st[nx][ny] && g[nx][ny] == 'W') {
dfs(nx, ny); // 递归访问新格子
}
}
}
int main() {
cin >> n >> m;
// 读入网格(注意:这里从1行1列开始存储,避免边界判断麻烦)
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> g[i][j];
}
}
int cnt = 0; // 统计水坑数量
// 遍历整个网格
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
// 找到未访问的W,触发Floodfill
if (!st[i][j] && g[i][j] == 'W') {
cnt++;
dfs(i, j);
}
}
}
cout << cnt << endl;
return 0;
}BFS 的核心是 “队列遍历”:从起点出发,把连通的 “W” 依次加入队列,逐层标记,逻辑更接近 “洪水扩散” 的直观过程。
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
typedef pair<int, int> PII; // 存储坐标(x,y)
const int N = 110;
char g[N][N];
bool st[N][N];
int n, m;
// 8方向方向数组(和DFS一致)
int dx[] = {0, 0, 1, -1, 1, 1, -1, -1};
int dy[] = {1, -1, 0, 0, 1, -1, 1, -1};
// BFS:从(x,y)出发,标记所有连通的W
void bfs(int x, int y) {
queue<PII> q; // 队列存储待标记的格子
q.push({x, y}); // 起点入队
st[x][y] = true; // 标记已访问
while (!q.empty()) { // 队列不为空,继续扩散
auto t = q.front();// 取出队头格子
q.pop();
int x = t.first, y = t.second;
// 遍历8个方向
for (int k = 0; k < 8; k++) {
int nx = x + dx[k];
int ny = y + dy[k];
// 边界+未访问+是水
if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && !st[nx][ny] && g[nx][ny] == 'W') {
st[nx][ny] = true; // 标记已访问
q.push({nx, ny}); // 新格子入队,等待后续扩散
}
}
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> g[i][j];
}
}
int cnt = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (!st[i][j] && g[i][j] == 'W') {
cnt++;
bfs(i, j);
}
}
}
cout << cnt << endl;
return 0;
}st[N][N]必须在访问前标记,否则会导致同一个格子被重复加入队列 / 递归,引发超时或栈溢出;题目链接:https://www.luogu.com.cn/problem/P1162

在 n×n 的 01 方阵中,有一个由 1 构成的闭合圈。要求把闭合圈内的所有 0 都填成 2。规则:如果从某个 0 出发,仅通过上下左右 4 个方向移动且只经过 0,无法到达方阵边界,那么这个 0 就在闭合圈内。
这道题是 Floodfill 的 “反向思维” 经典案例。直接找 “闭合圈内的 0” 很困难(因为你不知道哪个 0 被包围),但反过来想:能到达边界的 0 一定不在圈内,不能到达边界的 0 一定在圈内。
所以解题思路可以 “正难则反”:
为了简化边界判断,我们可以给原始网格 “外围加一层 0”(比如原始 n×n 网格,变成 (n+2)×(n+2) 网格),这样只需从 (0,0) 这个 “外套格子” 出发,一次 Floodfill 就能标记所有能到达边界的 0,无需单独遍历原始网格的四条边。
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
typedef pair<int, int> PII;
const int N = 35; // 题目中n≤30,加2层外套后35足够
int g[N][N]; // 存储01方阵(加外套后)
bool st[N][N]; // 标记“能到达边界的0”
int n;
// 4方向方向数组(本题是上下左右连通,无斜对角)
int dx[] = {0, 0, 1, -1};
int dy[] = {1, -1, 0, 0};
// BFS:从(x,y)出发,标记所有能到达边界的0
void bfs(int x, int y) {
queue<PII> q;
q.push({x, y});
st[x][y] = true; // 外套格子本身是0,标记为已访问
while (!q.empty()) {
auto t = q.front();
q.pop();
int x = t.first, y = t.second;
// 遍历4个方向
for (int k = 0; k < 4; k++) {
int nx = x + dx[k];
int ny = y + dy[k];
// 边界判断:nx在0~n+1,ny在0~n+1(外套的范围)
// 检查:未访问 + 是0(只有0能扩散)
if (nx >= 0 && nx <= n + 1 && ny >= 0 && ny <= n + 1 && !st[nx][ny] && g[nx][ny] == 0) {
st[nx][ny] = true;
q.push({nx, ny});
}
}
}
}
int main() {
cin >> n;
// 初始化加外套的网格(0~n+1行,0~n+1列)
memset(g, 0, sizeof g); // 外套部分默认是0
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> g[i][j]; // 读入原始网格(1~n行,1~n列)
}
}
// 从外套的(0,0)出发,标记所有能到达边界的0
bfs(0, 0);
// 输出结果:处理原始网格(1~n行,1~n列)
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (g[i][j] == 1) {
// 1保持不变
cout << 1 << " ";
} else if (st[i][j]) {
// 0且被标记:外圈0,保持不变
cout << 0 << " ";
} else {
// 0且未被标记:圈内0,填2
cout << 2 << " ";
}
}
cout << endl; // 每行结束换行
}
return 0;
}st[N][N]标记的是 “能到达边界的 0”,而非 “已访问的格子”—— 这是 Floodfill 的灵活应用,标记的是 “特定属性的区域”,而非单纯的访问状态;以题目中的示例输入为例:
输入(n=6):
000000
001111
011001
110001
100001
111111加外套后,BFS 从 (0,0) 出发,标记所有能到达边界的 0(如第一行的 0、第二行的 0 等);
输出时,未被标记的 0(圈内 0)被填成 2,最终结果如下:
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 通过上面两道例题,我们可以总结出 Floodfill 问题的通用解题框架,无论遇到什么连通块问题,都可以按这个步骤思考:
st[N][N]数组(但需注意题目是否允许修改输入)。st[N][N]或dist[N][N]未用 memset 重置,导致上一组数据的标记影响当前组;Floodfill 是算法入门的重要知识点,也是面试中的高频考点。掌握它,你不仅能解决 “连通块” 类问题,更能培养 “扩散思维” 和 “反向思维”,为后续学习更复杂的搜索算法(如 BFS 最短路径、DFS 剪枝)打下基础。 最后,留一个小思考题:如果网格中有两种 “水”(W1 和 W2),如何统计同时与 W1 和 W2 连通的旱地(.)数量?欢迎在评论区交流你的思路~