AcWing 1097池塘计数题解:手把手教你用BFS/DFS搞定Flood Fill,附C++代码调试技巧
AcWing 1097池塘计数从Flood Fill到竞赛实战的深度解析当你面对一片由W和.组成的矩阵时是否曾困惑如何高效统计其中的池塘数量这道看似简单的题目背后隐藏着图论中连通性问题解决的经典范式。Flood Fill算法正是解决这类问题的利器而BFS和DFS则是实现这一算法的双刃剑。1. Flood Fill算法本质与竞赛应用场景Flood Fill中文常译为泛洪算法或种子填充算法其核心思想类似于颜料在画布上的扩散过程。想象你有一桶油漆当你在某个点倾倒时颜料会向四周蔓延直到遇到边界或不同颜色的区域。在算法竞赛中这种思想被抽象为解决连通性问题的通用方法。Flood Fill在竞赛中的典型应用场景包括矩阵中连通区域的计数如本题的池塘统计图像处理中的颜色填充模拟游戏开发中的地图探索系统迷宫求解中的路径探索对于AcWing 1097这样的题目数据范围往往达到1000x1000这意味着我们需要一个时间复杂度为O(NM)的解决方案。Flood Fill配合BFS或DFS正好满足这一要求。2. BFS与DFS的选择策略与实现对比在解决连通性问题时BFS广度优先搜索和DFS深度优先搜索是两种最常见的实现方式。它们各有优劣理解这些差异对竞赛选手至关重要。2.1 BFS实现的特点与优势BFS采用队列数据结构按照距离起始点的远近层次遍历所有可达节点。对于本题的池塘计数问题BFS实现具有以下优势无递归栈溢出风险对于大规模矩阵如1000x1000DFS的递归实现可能导致栈溢出天然层级遍历可以方便地记录或计算遍历的深度代码结构清晰使用显式队列控制流程更加直观// BFS核心代码示例 void bfs(int x, int y) { queuePII q; q.push({x,y}); mp[x][y] .; while(!q.empty()) { auto t q.front(); q.pop(); for(int i0; i8; i) { int tx t.first dx[i]; int ty t.second dy[i]; if(tx0 ty0 txn tym mp[tx][ty]W) { mp[tx][ty] .; q.push({tx,ty}); } } } }2.2 DFS实现的适用场景尽管BFS在大多数情况下更优但DFS也有其适用场景代码更为简洁对于小规模数据或快速原型开发内存消耗略低不需要维护队列数据结构特定问题更自然如拓扑排序、回溯问题等// DFS核心代码示例 void dfs(int x, int y) { mp[x][y] .; for(int i0; i8; i) { int tx x dx[i]; int ty y dy[i]; if(tx0 ty0 txn tym mp[tx][ty]W) { dfs(tx, ty); } } }2.3 性能对比表格特性BFS实现DFS实现时间复杂度O(NM)O(NM)空间复杂度O(max(N,M))O(NM)最坏情况栈溢出风险无有代码复杂度中等简单适合数据规模大规模小规模额外功能支持距离计算方便回溯方便3. 代码实现中的关键细节与优化在实际编码过程中有几个关键细节需要特别注意它们往往是导致WAWrong Answer的罪魁祸首。3.1 方向数组的灵活定义本题中需要考虑8个方向的邻居上、下、左、右、左上、右上、左下、右下方向数组的定义方式直接影响代码的可读性和正确性。// 8方向数组定义 int dx[] {1,1,0,-1,-1,-1,0,1}; int dy[] {0,1,1,1,0,-1,-1,-1}; // 对应方向顺序 // 右、右下、下、左下、左、左上、上、右上提示方向数组的定义顺序不影响正确性但保持一致的顺序有助于调试和理解3.2 边界检查的三种实现方式边界检查是这类题目中最容易出错的部分常见的实现方式有三种显式条件判断if(tx0 ty0 txn tym ...)使用continue跳过if(tx0 || ty0 || txn || tym) continue;扩展边界法牺牲空间换代码简洁// 声明数组时多出2个元素 char mp[N2][M2]; // 初始化时将边界设为非W值3.3 访问标记的优化策略在Flood Fill过程中及时标记已访问的点至关重要。常见的标记方法有直接修改原矩阵本题采用的方法mp[x][y] .; // 将W改为.表示已访问使用独立的vis数组bool vis[N][M]; vis[x][y] true;位图压缩法对于极大矩阵bitsetN vis[M]; vis[x][y] 1;4. 调试技巧与常见错误分析即使理解了算法原理实际编码中仍会遇到各种问题。以下是针对本题的调试方法和常见错误。4.1 本地测试的实用技巧小数据测试先使用题目提供的样例测试边界测试测试N或M为1的情况全W测试整个矩阵都是W应输出1无W测试整个矩阵都是.应输出0随机生成测试编写随机数据生成器对拍// 简单的随机测试数据生成 srand(time(0)); int n rand()%10 1; int m rand()%10 1; cout n m endl; for(int i0; in; i) { for(int j0; jm; j) { cout (rand()%2 ? W : .); } cout endl; }4.2 在线提交时的常见错误数组越界未正确处理矩阵边界访问标记遗漏忘记标记已访问的点导致无限循环方向数组错误少考虑了某些方向或方向定义错误输入格式错误未注意字符间可能有空格本题明确说明没有变量未初始化如ans计数器未初始化为04.3 性能优化建议对于极端数据规模如1000x1000还需考虑以下优化输入输出加速ios::sync_with_stdio(false); cin.tie(0);使用更快的容器用普通数组代替STL队列减少函数调用将搜索函数内联位运算优化用位运算代替方向数组计算5. 从具体问题到通用解题框架掌握本题的解法后我们可以抽象出一个解决类似连通性问题的通用框架遍历矩阵中的每个元素当发现未访问的目标元素时计数器加1执行BFS/DFS标记所有连通区域输出计数器结果这个框架适用于许多变种问题例如统计岛屿数量LeetCode 200计算连通区域面积查找最大连通区域判断两点是否连通在最近的竞赛训练中我发现很多选手在处理8方向连通问题时容易遗漏对角线方向。一个实用的调试技巧是先用小规模数据如3x3矩阵测试所有方向的遍历是否正确。