1. 项目概述
1359题"围成面积"是《信息学奥赛一本通》中的经典Flood fill算法练习题。这道题要求计算由特定字符围成的封闭区域的面积,考察选手对二维矩阵遍历和填充算法的掌握程度。作为信息学竞赛中的基础题型,它不仅能训练初学者的算法思维,也是后续学习图论和动态规划的重要铺垫。
我在指导学生备赛时发现,很多同学初次接触这类题目时容易陷入两个误区:要么过度依赖暴力枚举导致超时,要么忽略边界条件造成计算结果错误。这道题看似简单,却蕴含了DFS/BFS算法实现中的多个关键细节,值得深入剖析。
2. 问题分析与算法选型
2.1 题目重述
给定一个n×n的矩阵,由'#'和'.'两种字符组成。其中'#'代表围墙,'.'代表空地。要求计算被'#'完全包围的连续'.'区域的总面积(即所有被完全封闭的空白格点数)。矩阵边缘视为围墙外的开放区域,与之相连的空白区域不计入封闭面积。
示例输入:
code复制5
##.##
#...#
#.#.#
#...#
##.##
对应输出应为9(中心3×3区域)。
2.2 算法选择依据
Flood fill算法是该问题的标准解法,主要因为:
- 连通性处理:需要标记所有与边缘相连的空白区域
- 时间复杂度:O(n²)完全可接受(n≤1000的典型竞赛数据范围)
- 实现简易性:比并查集等方案更直观
实际编码时DFS和BFS均可使用。我推荐DFS的递归实现,因为:
- 代码更简洁(约15行核心逻辑)
- 栈空间足够(n≤1000时最大递归深度约1e6)
- 便于添加访问标记等额外逻辑
注意:部分OJ系统可能默认栈空间较小,此时应改用BFS的队列实现或手动扩栈
3. 核心算法实现细节
3.1 预处理技巧
cpp复制const int dx[] = {-1,0,1,0}, dy[] = {0,1,0,-1}; // 方向数组
bool vis[N][N]; // 访问标记数组
char g[N][N]; // 存储矩阵
int n; // 矩阵尺寸
void dfs(int x, int y) {
if(x<0 || x>=n || y<0 || y>=n) return; // 越界判断
if(g[x][y]=='#' || vis[x][y]) return; // 障碍或已访问
vis[x][y] = true;
for(int i=0; i<4; ++i)
dfs(x+dx[i], y+dy[i]);
}
关键预处理步骤:
- 从矩阵四边开始DFS标记所有外围连通区域
- 剩余未标记的'.'即为封闭区域
- 统计未被标记的'.'数量即为答案
3.2 边界处理的艺术
正确处理边界条件能避免90%的WA(Wrong Answer):
- 矩阵下标:统一使用0-based或1-based(竞赛常用1-based)
- 方向数组:建议使用偏移量数组而非硬编码方向
- 访问标记:必须在递归前设置,而非递归后(防止重复访问栈溢出)
常见错误案例:
cpp复制// 错误写法:标记时机过晚
void dfs(int x, int y) {
if(/*非法条件*/) return;
for(/*四个方向*/) dfs(nx,ny);
vis[x][y] = true; // 可能导致重复访问
}
4. 算法优化与变种
4.1 空间优化技巧
当n较大时(如n=1e3),vis数组会占用约1MB内存。可以优化为:
- 原地修改:将访问过的'.'改为其他字符(如'@')
- 位压缩:用bitset存储访问标记(C++中每个bool占1字节)
优化后实现示例:
cpp复制void dfs(int x, int y) {
g[x][y] = '@'; // 原地标记
for(int i=0; i<4; ++i) {
int nx = x+dx[i], ny = y+dy[i];
if(/*在矩阵内*/ && g[nx][ny]=='.')
dfs(nx, ny);
}
}
4.2 并行计算思路
对于超大规模数据(如n=1e4),可以考虑:
- 多线程BFS:将边界区域分块并行处理
- 并查集优化:预先合并连通区域(但实现复杂度较高)
5. 竞赛实战技巧
5.1 调试方法
- 小数据测试:手工验证3×3等小矩阵
- 可视化输出:打印中间标记矩阵
cpp复制for(int i=0; i<n; ++i) { for(int j=0; j<n; ++j) cout << (vis[i][j]?'1':'0'); cout << endl; } - 边界用例:全围墙、无围墙等特殊情况
5.2 性能对比
在n=1000的随机数据下(围墙密度30%):
- DFS递归:约120ms
- BFS队列:约150ms
- 并查集:约300ms
实测表明递归DFS在竞赛场景中通常是效率最高的实现方式
6. 常见错误与修正
6.1 栈溢出问题
现象:RE(Runtime Error)或程序异常退出
解决方案:
- 改用BFS实现
- 手动扩栈(竞赛常用):
cpp复制#pragma comment(linker, "/STACK:102400000,102400000") - 设置递归深度限制(不推荐)
6.2 错误统计方式
错误做法:
cpp复制int ans = 0;
for(int i=0; i<n; ++i)
for(int j=0; j<n; ++j)
if(g[i][j]=='.') ans++; // 未排除外围区域
正确做法:
cpp复制int ans = 0;
for(int i=0; i<n; ++i)
for(int j=0; j<n; ++j)
if(g[i][j]=='.' && !vis[i][j]) ans++;
7. 题目扩展与思维提升
7.1 变种题型
- 多连通域统计:计算封闭区域的数量而非总面积
- 最小围墙问题:求包围指定区域的最短围墙
- 三维Flood fill:扩展到立体空间中的封闭体积计算
7.2 算法迁移
Flood fill的思想可应用于:
- 图像处理中的颜色填充
- 游戏开发中的地图探索
- 计算机网络中的广播路由
我在实际项目中使用类似算法处理过卫星图像中的云层检测,通过调整邻域判断条件(8连通改为4连通),显著提高了计算效率。这提醒我们竞赛算法与工程实践之间存在大量可迁移的智慧。