Flood Fill
定义
Flood Fill算法,又称为洪水填充或种子填充算法,是一种在图或网格数据结构中探索连通区域的搜索算法。它从一个初始节点(种子点)开始,将具有相同属性(如颜色、值等)的相邻节点递归或迭代地标记或改变属性值,直到遍历完所有与种子点连通且满足条件的节点。该算法因其形象地模拟了洪水填满低洼地带的过程而得名。
运用情况
- 图形编辑软件:用于实现颜色填充工具,用户点击图像上的一点后,相同颜色的相邻区域会被填充上新的颜色。
- 游戏开发:在游戏地图编辑或UI设计中,快速改变特定区域的颜色或纹理。
- 计算机视觉与图像处理:在图像分割、物体识别中,标识或隔离特定区域。
- 数据分析与可视化:在数据集上标记相连的相同值区域,帮助数据分析和可视化。
- 路径规划与地图绘制:在网格地图中寻找并标记可达区域或障碍区域。
注意事项
- 递归深度限制:使用递归实现时,需警惕栈溢出风险,可通过设置递归深度限制或改用迭代方法避免。
- 边界检查:确保算法不会超出数据结构的边界,避免访问未定义或非法内存区域。
- 效率与空间优化:利用访问标志数组减少重复访问,使用广度优先搜索(BFS)相比深度优先搜索(DFS)可能在空间效率上有优势。
- 颜色相似度判断:在某些应用场景中,可能需要基于颜色差异阈值来决定是否填充相邻像素。
- 并发处理:在并行或分布式系统中实施时,需要适当的锁机制或任务划分策略以避免冲突。
解题思路
- 初始化:选择起始节点(种子点)和目标属性值(如颜色)。
- 选择搜索策略:决定使用DFS还是BFS。DFS更易于实现但可能导致较深的递归调用;BFS则更适合求解最短路径问题,且在大多数情况下空间效率较高。
- 创建工作队列/栈:根据所选策略准备数据结构来存储待处理节点。
- 标记与扩展:将种子点标记为已访问,并将其相邻且属性相同的节点加入队列/栈。
- 循环处理:循环执行以下步骤直至队列/栈为空:从队列/栈中取出一个节点,修改其属性(如颜色),并将符合条件的相邻未访问节点加入队列/栈。
- 结束条件:当所有与种子点连通且符合条件的节点都被处理过后,算法结束。
AcWing 1097. 池塘计数
题目描述
AcWing 1097. 池塘计数 - AcWing
运行代码
#include <iostream>
#include <vector>
using namespace std;
int n, m;
vector<vector<char>> grid;
void dfs(int i, int j) {if (i < 0 || i >= n || j < 0 || j >= m || grid[i][j]!= 'W') {return;}grid[i][j] = '.';dfs(i - 1, j);dfs(i + 1, j);dfs(i, j - 1);dfs(i, j + 1);dfs(i - 1, j - 1);dfs(i + 1, j + 1);dfs(i - 1, j + 1);dfs(i + 1, j - 1);
}
int countPonds() {int ponds = 0;for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {if (grid[i][j] == 'W') {ponds++;dfs(i, j);}}}return ponds;
}
int main() {cin >> n >> m;grid.resize(n, vector<char>(m));for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {cin >> grid[i][j];}}cout << countPonds() << endl;return 0;
}
代码思路
-
输入处理:
- 首先,读取网格的行数
n
和列数m
。 - 然后,根据
n
和m
的值,初始化一个二维向量grid
来存储地图信息。每一行是一个包含m
个元素的字符向量,表示地图的一行。 - 接着,逐行读取每个单元格的状态(字符),填充到
grid
中。
- 首先,读取网格的行数
-
深度优先搜索(DFS):
- 定义函数
dfs(int i, int j)
,用于从地图上的位置(i, j)
开始,遍历并标记相连的所有水池单元格。它通过递归地访问当前点的上、下、左、右以及对角线相邻的点,如果那些点是水池(即字符为'W'),则标记为已访问(改为'.')并继续深入搜索。 - 注意这里考虑了八个方向的邻居,包括水平、垂直和两个对角线方向,实现了对角落和边缘连接的全面搜索。
- 定义函数
-
计算水汽数量:函数
countPonds()
遍历整个网格,每当遇到一个未被访问的水池(字符'W'),就调用dfs()
函数进行填充,并累加计数器ponds
。这样,最终的ponds
值就是地图中独立水池的数量。 -
输出结果:主函数最后输出计算得到的水汽数量。
改进思路
-
使用迭代而非递归:深度优先搜索虽然逻辑直观,但是递归可能会导致较大的栈空间消耗,特别是对于大型网格。可以通过使用栈数据结构来实现迭代版本的DFS,从而避免栈溢出的风险。
-
增加记忆化或标记已访问:尽管代码中通过将'W'改为'.'来标记已访问,但这种做法改变了原始地图。可以引入一个额外的二维布尔数组
visited[n][m]
来记录每个位置是否已被访问过,这样既不影响原地图,也能有效避免重复访问,提高效率。 -
广度优先搜索(BFS)替代DFS:在某些情况下,使用BFS代替DFS可能更有利,特别是当需要按距离排序(比如找出最近的水池)或者关心最短路径时。BFS使用队列进行层次遍历,可以更容易地控制搜索的范围和顺序。
-
并行处理:如果网格非常大,可以考虑将网格分割成多个小块,并在不同的线程或进程中并行执行DFS或BFS,最后汇总结果。但需要注意同步访问共享资源,防止数据竞争。
-
优化搜索方向:在某些特定场景下,不是所有八个方向都需要检查。例如,如果知道水体只能水平或垂直流动(非对角线),则可以减少搜索方向,提高效率。
-
使用并查集数据结构:对于频繁的连通性查询,使用并查集可以在合并连通分量时提供高效的O(α(n))操作(其中α(n)是一个非常接近于1的函数),有助于提升处理大量连通组件的效率。
-
动态调整搜索范围:根据具体问题,可能不需要全面八方向搜索。例如,如果水域只能在平面上水平或垂直流动,可以只检查上下左右四个方向。
其它代码
#include <iostream>
#include <cstring>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 1010;
int n, m;
int ans;
char g[N][N];
PII p[N * N];
bool st[N][N];
void bfs(int sx, int sy)
{int hh = 0, tt = -1;p[++ tt] = {sx, sy};st[sx][sy] = true;while(hh <= tt){PII t = p[hh ++];for(int i = t.x - 1; i <= t.x + 1; i ++ )for(int j = t.y - 1; j <= t.y + 1; j ++ ){if(i < 0 || i >= n || j < 0 || j >= m) continue;if(st[i][j] || g[i][j] == '.') continue;st[i][j] = true;p[++ tt] = {i, j};}}
}
int main()
{cin >> n >> m;for(int i = 0; i < n; i ++ ) cin >> g[i];for(int i = 0; i < n; i ++ )for(int j = 0; j < m; j ++ )if(!st[i][j] && g[i][j] == 'W'){bfs(i, j);ans ++ ;}cout << ans << endl;return 0;
}
代码思路
-
数据结构与变量定义:
- 使用
#define
宏定义了坐标对的引用方式,便于后续操作。 - 定义了常量
N
作为最大行数/列数的限制,以及二维数组g
来存储地图信息,布尔型二维数组st
来标记某个位置是否已经被访问过。 PII
是一个类型别名,表示一对整数(坐标),用于存储队列中的位置信息。ans
变量用来累计水池的数量。
- 使用
-
主函数(main):
- 首先读取地图的行数
n
和列数m
,然后逐行读取地图信息到二维数组g
中。 - 双重循环遍历整个地图,对于每一个未访问且为水池的格子,调用
bfs
函数进行广度优先搜索,同时增加水池计数器ans
的值。
- 首先读取地图的行数
-
广度优先搜索(bfs)函数:
- 初始化队列,将当前水池的起始坐标
(sx, sy)
入队,并标记为已访问。 - 当队列不为空时,循环处理队首元素,遍历其周围的8个相邻格子(注意代码中实际访问了9个位置,包括自身,但注释掉的那行代码表明本意是排除自身)。
- 对于每个有效的相邻格子(未越界且未访问且为水池),将其标记为已访问,并将其坐标入队。
- 这一过程会持续到当前水池区域的所有可达位置都被访问过,从而完成了单个水池的计数。
- 初始化队列,将当前水池的起始坐标
-
输出结果:最后,输出统计到的水池总数
ans
。