洪水填充
洪水填充算法(flood fill algorithm),也称为泛洪算法,用于将格点的某一个连通区域内的所有格点状态修改为目标状态,状态往往用颜色表示。一般的处理方法是,从一个起始点开始把附近与其连通的点填充成新的颜色,直到连通区域内的所有点都被处理过为止,因为其思路类似洪水从一个区域扩散到所有能到达的其他区域而得名。
DFS、BFS 都可以用来实现洪水填充算法,常见的邻域包括四邻域和八邻域等。

例题:P1162 填涂颜色
给定一个 \(n \times n \ (1 \le n \le 30)\) 的方阵,其中包含由数字 \(1\) 围成的一个封闭圈,其余位置为数字 \(0\),保证方阵内只有一个闭合圈,且圈内至少包含一个 \(0\)。现要求找出所有被 \(1\) 包围在闭合圈内部的 \(0\),并将它们的值修改为 \(2\),最后要求输出修改后的方阵。
直接寻找“位于闭合圈内部的 \(0\)”可能会比较繁琐,因为闭合圈的形状是任意的。然而,可以从反向思维出发。所有无法从方阵边界到达的 \(0\),必然处于闭合圈内部。反之,凡是能够通过四连通方向(上下左右)与矩阵边界或外部相连通的 \(0\),都属于闭合圈外部的 \(0\)。因此,问题可以转化为:找到所有在圈外的 \(0\),剩下的 \(0\) 即为圈内需要修改为 \(2\) 的目标点。
为了防止边界上的 \(0\) 无法被统一步骤处理,可以在原本的 \(n \times n\) 的方阵外围增加一层“保护圈”,即扩展下标范围至 \(0 \sim n+1\)。从坐标 \((0,0)\) 出发进行搜素,即可顺畅地把所有“圈外”的 \(0\) 全部标记出来。
参考代码(BFS)
#include <iostream>
#include <queue>
using namespace std;
using pi = pair<int, int>;
const int N = 35;
// 定义上下左右四个方向
const int DX[4] = {-1, 1, 0, 0};
const int DY[4] = {0, 0, -1, 1};
int a[N][N];
bool vis[N][N];
int main()
{
int n; cin >> n;
// 读取 1 到 n 范围内的矩阵
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> a[i][j];
}
}
// 从外围点 (0, 0) 开始做 BFS 遍历圈外的 0
queue<pi> q;
q.push({0, 0});
vis[0][0] = true;
while (!q.empty()) {
pi u = q.front();
q.pop();
for (int i = 0; i < 4; i++) {
int x = u.first + DX[i], y = u.second + DY[i];
// 检查边界范围 [0, n + 1]
if (x >= 0 && x <= n + 1 && y >= 0 && y <= n + 1) {
// 如果未访问过且是 0,说明属于圈外的连通区域
if (!vis[x][y] && a[x][y] == 0) {
vis[x][y] = true;
q.push({x, y});
}
}
}
}
// 遍历目标区域 [1, n],把圈内未标记的 0 改成 2
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (a[i][j] == 0 && !vis[i][j]) {
a[i][j] = 2;
}
cout << a[i][j] << " ";
}
cout << "\n";
}
return 0;
}
外层网格大小为 \((n+2) \times (n+2)\),BFS 搜索过程中每个格子最多入队并被访问一次,时间复杂度为 \(O(n^2)\)。
参考代码(DFS)
#include <iostream>
using namespace std;
const int N = 35;
int n, a[N][N];
// 深度优先搜索函数
void dfs(int x, int y) {
// 越界检查(边界为 0 到 n + 1)
if (x < 0 || x > n + 1 || y < 0 || y > n + 1) return;
// 如果不是 0,说明遇到障碍物 1 或者已标记过的 3,终止递归
if (a[x][y] != 0) return;
// 将圈外的 0 标记为 3,防止重复搜索并起到标记作用
a[x][y] = 3;
// 向上下左右四个方向延伸
dfs(x - 1, y);
dfs(x + 1, y);
dfs(x, y - 1);
dfs(x, y + 1);
}
int main()
{
cin >> n;
// 读取 1 到 n 范围内的矩阵,未读取的 0 行 0 列及 n+1 行 n+1 列默认值为 0
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> a[i][j];
}
}
// 从外围虚拟节点 (0, 0) 开始做 DFS,搜索标记所有圈外的 0
dfs(0, 0);
// 遍历原始矩阵区域 [1, n],进行分类输出
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (a[i][j] == 0) {
cout << "2 "; // 圈内的 0 变为 2
} else if (a[i][j] == 3) {
cout << "0 "; // 圈外的 0 还原为 0
} else {
cout << "1 "; // 围墙 1 保持不变
}
}
cout << "\n";
}
return 0;
}
扩展后的网格大小为 \((n+2) \times (n+2)\),DFS 搜索过程中每个格子最多被递归访问一次,时间复杂度为 \(O(n^2)\)。
习题:P1596 [USACO10OCT] Lake Counting S
给定一个 \(N \times M \ (1 \le N,M \le 100)\) 的网格矩阵,网格中的每个格子要么是水
W,要么是干地.。如果两个W在八个方向(上下左右及四个对角线方向)相邻,则认为它们属于同一个水塘,求网格中形成的水塘(连通块)总数。
解题思路
对于网格上的每一个点:
- 如果当前格子是水
W且未被访问,说明发现了水塘的一个新起点。使得水塘数量计数器加 \(1\),并从该点出发发起一次搜索,将与该点八连通的所有W全部标记为已访问。 - 如果当前格子是干地
.或者已经被访问标记过,则跳过。
参考代码
#include <iostream>
using namespace std;
const int N = 105;
// 定义八个方向的坐标偏移量(包含上下左右与四个对角线)
const int X[] = {-1, -1, -1, 0, 0, 1, 1, 1};
const int Y[] = {-1, 0, 1, -1, 1, -1, 0, 1};
int n, m;
char g[N][N]; // 存储网格字符
bool vis[N][N]; // 标记数组,记录点是否已被搜索访问过
// 深度优先搜索(DFS)遍历连通块
void dfs(int x, int y) {
vis[x][y] = true; // 标记当前水域为已访问
// 尝试向 8 个相邻方向扩展
for (int i = 0; i < 8; i++) {
int nx = x + X[i], ny = y + Y[i];
// 边界检查、未访问检查以及是否为水域检查
if (nx >= 0 && nx < n && ny >= 0 && ny < m && !vis[nx][ny] && g[nx][ny] == 'W') {
dfs(nx, ny);
}
}
}
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i++) {
cin >> g[i];
}
int ans = 0; // 记录水塘(连通块)数量
// 遍历整个网格
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
// 当发现未访问过的水域时,找到一个新的水塘
if (g[i][j] == 'W' && !vis[i][j]) {
dfs(i, j); // 通过 DFS 标记整个连通的水塘
ans++; // 水塘数量加 1
}
}
}
cout << ans << "\n";
return 0;
}
网格中每个格子最多只会被 DFS 访问和标记一次,对于每个格子,程序最多检查其周围的 8 个方向。因此总体时间复杂度与网格大小成正比,为 \(O(N \times M)\)。
例题:P1902 刺杀大使
在一个 \(n \times m \ (n,m \le 1000)\) 的网格迷阵中,需要从第 \(1\) 行(入口)出发,到达第 \(n\) 行的所有房间以打开全部机关。每个房间 \((i, j)\) 有一个伤害值 \(p_{i, j} \ (p_{i,j} \le 1000)\)(第 \(1\) 行和第 \(n\) 行伤害值均为 \(0\))。寻找一条从第 \(1\) 行到第 \(n\) 行的路径,使得该路径上通过的房间的最大伤害值最小。
解题思路
本题的求值目标是“使最大值最小”,这是非常典型的二分答案标志。同时,答案显然具有单调性:
- 如果允许的最大伤害为 \(X\) 时,能够顺利从第 \(1\) 行到达第 \(n\) 行;那么当允许的最大伤害大于 \(X\) 时,也一定能够到达。
- 如果允许的最大伤害为 \(X\) 时无法到达,那么允许伤害更小时也必定无法到达。
因此,可以在 \([0, \max(p_{i,j})]\) 的范围内二分搜索这个最小的伤害代价 \(p\)。对于二分的每一个中间值 \(p\),需要判断:在只经过伤害值 \(\le p\) 的房间的前提下,能否从第一行到达最后一行?这可以转化为一个简单的洪水填充问题。
时间复杂度为 \(O(n \times m \log(\max p))\)。
参考代码
#include <iostream>
#include <algorithm>
#include <queue>
#include <utility>
using namespace std;
const int N = 1005;
// 方向数组,控制上下左右四个方向移动
int dx[] = {-1, 1, 0, 0};
int dy[] = {0, 0, -1, 1};
int n, m, p[N][N];
bool vis[N][N]; // 标记数组,防止 BFS 重复访问
// 检查在最大伤害限制为 val 的情况下,能否从第一行到达最后一行
bool check(int val) {
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
vis[i][j] = false;
queue<pair<int, int>> q;
// 第一行的所有点都可以作为起点
for (int i = 1; i <= m; i++) {
q.push({1, i});
vis[1][i] = true;
}
while (!q.empty()) {
int x = q.front().first, y = q.front().second;
q.pop();
// 只要到达了第 n 行,说明当前限制 val 可行
if (x == n) return true;
// 向四个方向扩展
for (int i = 0; i < 4; i++) {
int nx = x + dx[i], ny = y + dy[i];
// 边界判断
if (nx >= 1 && nx <= n && ny >= 1 && ny <= m) {
// 只有未访问过且目标房间伤害值 <= val 时才能通行
if (!vis[nx][ny] && p[nx][ny] <= val) {
vis[nx][ny] = true;
q.push({nx, ny});
}
}
}
}
return false;
}
int main()
{
cin >> n >> m;
int maxp = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
cin >> p[i][j];
maxp = max(maxp, p[i][j]); // 记录最大伤害值作为二分的右边界
}
// 二分搜索答案,范围在 [0, maxp]
int low = 0, high = maxp, ans = maxp;
while (low <= high) {
int mid = low + (high - low) / 2;
if (check(mid)) {
ans = mid; high = mid - 1; // 记录可行解,尝试寻找更小的可行解
} else {
low = mid + 1; // 伤害限制太小,需要调大限制
}
}
cout << ans << "\n";
return 0;
}

浙公网安备 33010602011771号