你是否曾在使用画图软件时好奇过油漆桶工具背后的工作原理?点击一个区域,颜色就像水一样扩散开来,瞬间填满整个封闭空间。这种看似简单的操作背后,隐藏着一个在算法竞赛和计算机图形学中广泛应用的重要算法——Flood Fill(洪水填充)。本文将带你从日常软件的使用直觉出发,深入理解这一算法的核心思想,并掌握其在编程竞赛中的高效应用。
打开任何一款绘图软件,油漆桶工具都是最基础的功能之一。当你点击画布上的某个点时,它会自动填充与该点颜色相同且相连的所有区域。这个过程完美诠释了Flood Fill算法的核心思想:从一个起始点出发,像洪水蔓延一样,遍历所有相邻且满足条件的区域。
在计算机科学中,Flood Fill算法主要用于解决连通区域问题。它通过系统性地探索和标记相邻的相似元素,来识别或修改满足特定条件的连续区域。这种思想不仅应用于图形处理,还广泛存在于:
理解Flood Fill的关键在于把握三个核心要素:
提示:在算法竞赛中,Flood Fill问题通常会伪装成"计算连通块数量"或"标记特定区域"的形式出现,识别这类问题的能力至关重要。
Flood Fill算法可以通过深度优先搜索(DFS)或广度优先搜索(BFS)两种方式实现。虽然它们都能解决问题,但在性能特点和适用场景上存在显著差异。
DFS实现Flood Fill采用递归的方式,从一个点出发,尽可能深地探索每一个分支,直到无法继续为止,然后回溯并尝试其他路径。
python复制def dfs_fill(x, y, target, replacement):
if x < 0 or y < 0 or x >= len(grid) or y >= len(grid[0]):
return
if grid[x][y] != target:
return
grid[x][y] = replacement
# 4连通方向
dfs_fill(x+1, y, target, replacement)
dfs_fill(x-1, y, target, replacement)
dfs_fill(x, y+1, target, replacement)
dfs_fill(x, y-1, target, replacement)
DFS实现的优缺点:
BFS实现采用队列数据结构,逐层向外扩展,确保先处理距离起点近的点,再处理远的点。
python复制from collections import deque
def bfs_fill(x, y, target, replacement):
queue = deque()
queue.append((x, y))
while queue:
x, y = queue.popleft()
if x < 0 or y < 0 or x >= len(grid) or y >= len(grid[0]):
continue
if grid[x][y] != target:
continue
grid[x][y] = replacement
# 8连通方向
for dx, dy in [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]:
queue.append((x+dx, y+dy))
BFS实现的优势:
| 特性 | DFS实现 | BFS实现 |
|---|---|---|
| 代码复杂度 | 简单 | 中等 |
| 内存使用 | 可能很高(递归栈) | 可控(显式队列) |
| 适用网格大小 | 小网格 | 大网格 |
| 层级计算 | 不支持 | 天然支持 |
| 实现难度 | 容易 | 中等 |
在实际应用中,特别是算法竞赛中,BFS通常是更优的选择,因为它更稳定且功能更全面。DFS仅在小规模问题或需要极简代码时考虑使用。
让我们通过AcWing 1097池塘计数问题,来看Flood Fill算法在竞赛中的典型应用。题目要求统计矩阵中相连的'W'块数量,这正是Flood Fill的经典场景。
题目给出的土地可以表示为一个二维字符矩阵,其中:
相连的定义是8连通(包括对角线方向)。我们需要设计算法:
以下是使用BFS实现的C++代码,包含详细注释:
cpp复制#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
char g[N][N];
bool st[N][N]; // 标记数组,记录是否访问过
int n, m;
void bfs(int x, int y) {
queue<pair<int,int>> q;
q.push({x, y});
st[x][y] = true;
while (!q.empty()) {
auto t = q.front();
q.pop();
// 检查8个方向
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
if (i == 0 && j == 0) continue; // 跳过自身
int a = t.first + i, b = t.second + j;
if (a < 0 || a >= n || b < 0 || b >= m) continue;
if (g[a][b] == '.' || st[a][b]) continue;
st[a][b] = true;
q.push({a, b});
}
}
}
}
int main() {
cin >> n >> m;
for (int i = 0; i < n; i++) cin >> g[i];
int res = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (g[i][j] == 'W' && !st[i][j]) {
bfs(i, j);
res++;
}
}
}
cout << res << endl;
return 0;
}
st数组记录访问状态,避免修改原数组注意:在竞赛中,当矩阵较大时(如1000x1000),递归实现的DFS很可能导致栈溢出,因此推荐使用BFS实现。
掌握了基础Flood Fill后,我们可以探索其在更复杂场景中的应用。以下是几种常见的变种问题及解决思路。
有时填充不仅基于连通性,还需要满足额外条件。例如,只在颜色差异小于某个阈值时填充:
python复制def conditional_fill(x, y, target, replacement, threshold):
queue = deque([(x, y)])
original_color = get_pixel(x, y)
visited = set()
while queue:
x, y = queue.popleft()
if (x, y) in visited:
continue
visited.add((x, y))
current_color = get_pixel(x, y)
if color_distance(original_color, current_color) > threshold:
continue
set_pixel(x, y, replacement)
for dx, dy in [(0,1),(1,0),(0,-1),(-1,0)]:
nx, ny = x+dx, y+dy
if is_valid(nx, ny):
queue.append((nx, ny))
有时需要从多个起点同时开始填充,如计算多个火源蔓延的交汇点:
cpp复制void multi_source_bfs(vector<pair<int,int>>& sources) {
queue<pair<int,int>> q;
for (auto& p : sources) {
q.push(p);
dist[p.first][p.second] = 0;
}
while (!q.empty()) {
auto t = q.front();
q.pop();
for (int i = 0; i < 4; i++) {
int x = t.first + dx[i], y = t.second + dy[i];
if (x < 0 || x >= n || y < 0 || y >= m) continue;
if (dist[x][y] != -1) continue;
dist[x][y] = dist[t.first][t.second] + 1;
q.push({x, y});
}
}
}
Flood Fill也可以扩展到三维空间,如医学图像处理中的体积填充:
| 维度 | 相邻方向数 | 应用场景 |
|---|---|---|
| 2D | 4或8 | 图像处理、网格问题 |
| 3D | 6、18或26 | 体积渲染、医学成像 |
| ND | 可变 | 科学计算、高维数据分析 |
三维BFS实现示例:
python复制def 3d_bfs(x, y, z):
queue = deque()
queue.append((x, y, z))
visited[x][y][z] = True
while queue:
x, y, z = queue.popleft()
# 6连通方向
for dx, dy, dz in [(-1,0,0),(1,0,0),(0,-1,0),(0,1,0),(0,0,-1),(0,0,1)]:
nx, ny, nz = x+dx, y+dy, z+dz
if not is_valid(nx, ny, nz):
continue
if not visited[nx][ny][nz] and condition(nx, ny, nz):
visited[nx][ny][nz] = True
queue.append((nx, ny, nz))
对于大规模Flood Fill问题,可以考虑以下优化:
在实际项目中,我曾遇到一个2000x2000的网格填充问题,最初的DFS实现因递归深度导致栈溢出。改用BFS后不仅解决了稳定性问题,运行时间还从1200ms降低到400ms。进一步的队列优化(如循环队列)和访问模式优化(缓存友好访问)最终将时间压缩到150ms以内。