1. 问题背景与算法选型
洛谷P1162是一道经典的图论练习题,题目要求在一个由0和1组成的二维矩阵中,将未被1包围的0标记为2。这类矩阵填充问题在实际应用中非常常见,比如图像处理中的区域填充、GIS系统中的地形标记等。
Flood fill(洪水填充)算法是这个场景下的自然选择。它就像用油漆桶工具进行区域填充:从一个起始点出发,向四周扩散标记所有连通区域。但题目有个关键陷阱——直接对矩阵内的0进行填充会错误处理与边界相连的区域。
边界扩展技巧是这个问题的点睛之笔。通过在矩阵外围人工添加一圈0,原本与真实边界相连的"外圈0"就变成了内部连通区域,而真正被1包围的"内圈0"则保持隔离状态。
2. 边界扩展的实现细节
2.1 矩阵预处理
标准的N×N矩阵需要扩展为(N+2)×(N+2)。以Python为例:
def expand_matrix(matrix): n = len(matrix) # 上下各加一行0 new_matrix = [[0]*(n+2)] for row in matrix: new_matrix.append([0] + row.copy() + [0]) new_matrix.append([0]*(n+2)) return new_matrix这种处理有三大优势:
- 统一处理边界条件,避免复杂的越界判断
- 将"是否接触真实边界"转化为"是否连通到人工边界"
- 保持原有矩阵坐标不变(原矩阵[0][0]对应新矩阵[1][1])
2.2 填充方向的选择
四向填充(上、下、左、右)和八向填充会产生不同结果。本题应采用四向填充,因为:
- 八向填充会通过斜角连接区域,可能导致过度填充
- 符合题目要求的"上下左右四方向连通"定义
directions = [(-1,0), (1,0), (0,-1), (0,1)] # 四向移动向量3. Flood fill的两种实现方式
3.1 DFS递归实现
深度优先搜索写法简洁,但需要注意:
- Python默认递归深度限制(约1000层)可能爆栈
- 对于100×100的矩阵,递归实现可能不够稳健
def dfs_fill(x, y, matrix): if matrix[x][y] != 0: return matrix[x][y] = 2 for dx, dy in directions: dfs_fill(x+dx, y+dy, matrix)3.2 BFS队列实现
广度优先搜索更适合大规模数据:
- 使用队列显式管理待处理节点
- 不受递归深度限制
- 可以方便地加入剪枝逻辑
from collections import deque def bfs_fill(start_x, start_y, matrix): queue = deque([(start_x, start_y)]) while queue: x, y = queue.popleft() if matrix[x][y] != 0: continue matrix[x][y] = 2 for dx, dy in directions: nx, ny = x+dx, y+dy if 0 <= nx < len(matrix) and 0 <= ny < len(matrix[0]): queue.append((nx, ny))4. 完整解题流程
4.1 算法步骤拆解
- 矩阵边界扩展(N×N → (N+2)×(N+2))
- 从人工边界上的(0,0)点启动Flood fill
- 填充所有连通的外围0为2
- 收缩矩阵去除人工边界
- 遍历矩阵:剩余0即为被1包围的区域,应改为2;已标记2的恢复为0
4.2 代码实现示例
def solve(): n = int(input()) original = [list(map(int, input().split())) for _ in range(n)] # 边界扩展 expanded = expand_matrix(original) # 从(0,0)开始填充外围0 bfs_fill(0, 0, expanded) # 处理结果 for i in range(1, n+1): for j in range(1, n+1): if expanded[i][j] == 0: original[i-1][j-1] = 2 elif expanded[i][j] == 2: original[i-1][j-1] = 0 # 输出结果 for row in original: print(' '.join(map(str, row)))5. 调试技巧与常见错误
5.1 典型错误案例
忘记处理人工填充的2还原为0:
- 会导致输出矩阵中出现错误的2
- 解决方法:在收缩矩阵时明确处理三种状态(原始1、被包围0、外围0)
矩阵坐标转换错误:
- 扩展前后矩阵索引偏移1位
- 建议:在关键位置打印矩阵快照验证
5.2 测试用例设计
有效测试用例应包含:
- 全1矩阵(应无变化)
- 全0矩阵(应全部标记2)
- 螺旋形1包围0的复杂案例
- 0接触边界的特殊情况
例如:
输入: 3 1 1 1 1 0 1 1 1 1 正确输出: 1 1 1 1 2 1 1 1 16. 算法优化与变种
6.1 性能优化方向
并查集(Union-Find)实现:
- 预处理时将相邻0合并
- 最后检查哪些集合与边界连通
- 适合超大规模稀疏矩阵
多线程填充:
- 将矩阵分块并行处理
- 需要处理块间边界同步
6.2 实际应用变种
图像处理中的背景移除:
- 将边界扩展理解为画布外背景
- 填充所有与背景连通的区域
迷宫生成与求解:
- 墙相当于矩阵中的1
- 通路相当于0
- 找出所有不可达区域
7. 不同语言的实现差异
7.1 C++实现要点
// 使用vector<vector<int>>存储矩阵 // BFS实现时建议使用STL queue // 注意矩阵访问效率:行优先遍历 void bfsFill(int x, int y, vector<vector<int>>& matrix) { queue<pair<int,int>> q; q.push({x,y}); while(!q.empty()) { auto [cx,cy] = q.front(); q.pop(); if(matrix[cx][cy] != 0) continue; matrix[cx][cy] = 2; for(auto [dx,dy] : directions) { int nx = cx + dx, ny = cy + dy; if(nx>=0 && nx<matrix.size() && ny>=0 && ny<matrix[0].size()) q.push({nx,ny}); } } }7.2 JavaScript实现特点
// 注意矩阵用二维数组表示 // 可以使用Array.prototype.flat()简化处理 // 异步编程模型适合实现可视化填充过程 async function animateFill(matrix, x, y) { if(matrix[x][y] !== 0) return; matrix[x][y] = 2; // 可加入延迟实现动画效果 await new Promise(r => setTimeout(r, 100)); for(const [dx, dy] of directions) { const nx = x + dx, ny = y + dy; if(nx>=0 && nx<matrix.length && ny>=0 && ny<matrix[0].length) await animateFill(matrix, nx, ny); } }8. 从算法题到工程实践
8.1 工业级实现考量
内存优化:
- 对于超大矩阵,使用位图压缩存储
- 分块加载处理(类似地理信息系统中的瓦片地图)
填充策略选择:
- 扫描线填充算法更适合GPU并行
- 对于稀疏区域可使用四叉树加速
8.2 错误处理增强
输入验证:
- 矩阵尺寸一致性检查
- 非法值过滤(非0/1输入)
健壮性改进:
- 设置最大迭代次数防止死循环
- 内存不足时的优雅降级处理
我在实际编码比赛中发现,这类题目往往有隐藏的边界条件测试用例。比如N=1时的极端情况,或者矩阵中所有0都连通的情况。建议在提交前用以下案例自测:
1 0 → 应输出2 2 1 0 0 1 → 应保持不变