1. 题目解析与核心思路拆解
力扣130题"被围绕的区域"是一个经典的矩阵遍历问题,题目要求我们将所有被'X'完全包围的'O'区域替换为'X'。这个看似简单的问题背后蕴含着DFS/BFS算法的精妙应用,也是面试中考察候选人图遍历能力的常见题型。
1.1 问题本质理解
题目给出的矩阵可以抽象为一个二维网格图,其中:
- 'O'代表可通行节点
- 'X'代表障碍物节点
关键点在于识别"被完全包围"的定义:一个'O'区域如果没有任何一个节点位于矩阵边界上,那么它就是被完全包围的。换句话说,所有与边界相连的'O'区域及其连通区域都应该被保留。
1.2 逆向思维的应用
直接寻找被包围的区域比较困难,更聪明的做法是:
- 先标记所有不会被包围的'O'区域(即与边界相连的区域)
- 然后将剩下的'O'全部替换为'X'
- 最后恢复被标记的特殊区域
这种"逆向标记法"是解决此类问题的经典范式,可以避免不必要的重复遍历和复杂判断。
2. 深度优先搜索(DFS)实现详解
2.1 算法框架设计
DFS是解决矩阵连通性问题最直观的方法。具体步骤如下:
python复制def solve(board):
if not board:
return
rows, cols = len(board), len(board[0])
# 步骤1:标记边界'O'及其连通区域
for i in range(rows):
for j in range(cols):
if (i in [0, rows-1] or j in [0, cols-1]) and board[i][j] == 'O':
dfs(board, i, j)
# 步骤2:替换剩余'O'为'X',恢复标记区域
for i in range(rows):
for j in range(cols):
if board[i][j] == 'O':
board[i][j] = 'X'
elif board[i][j] == '#':
board[i][j] = 'O'
def dfs(board, i, j):
if i < 0 or j < 0 or i >= len(board) or j >= len(board[0]) or board[i][j] != 'O':
return
board[i][j] = '#' # 临时标记
dfs(board, i+1, j)
dfs(board, i-1, j)
dfs(board, i, j+1)
dfs(board, i, j-1)
2.2 关键实现细节
- 边界条件处理:必须先检查矩阵是否为空,避免后续访问越界
- 标记策略选择:使用特殊字符'#'标记需要保留的'O',避免与原始值冲突
- 递归终止条件:包括越界检查和节点类型检查
- 四方向遍历:必须覆盖上下左右四个相邻节点
注意:在实际面试中,务必先和面试官确认是否可以修改输入矩阵。如果不能修改,需要额外使用visited矩阵记录访问状态。
3. 广度优先搜索(BFS)实现方案
3.1 BFS算法实现
对于大规模矩阵,DFS可能引发栈溢出,此时BFS是更安全的选择:
python复制from collections import deque
def solve(board):
if not board:
return
rows, cols = len(board), len(board[0])
queue = deque()
# 将边界'O'加入队列
for i in range(rows):
for j in [0, cols-1]:
if board[i][j] == 'O':
queue.append((i,j))
board[i][j] = '#'
for j in range(cols):
for i in [0, rows-1]:
if board[i][j] == 'O':
queue.append((i,j))
board[i][j] = '#'
# BFS遍历
directions = [(-1,0),(1,0),(0,-1),(0,1)]
while queue:
i, j = queue.popleft()
for di, dj in directions:
ni, nj = i+di, j+dj
if 0 <= ni < rows and 0 <= nj < cols and board[ni][nj] == 'O':
board[ni][nj] = '#'
queue.append((ni,nj))
# 最终替换
for i in range(rows):
for j in range(cols):
if board[i][j] == 'O':
board[i][j] = 'X'
elif board[i][j] == '#':
board[i][j] = 'O'
3.2 BFS与DFS的选择考量
| 特性 | DFS | BFS |
|---|---|---|
| 空间复杂度 | O(mn)最坏情况 | O(min(m,n)) |
| 适用场景 | 小规模矩阵 | 大规模矩阵 |
| 实现难度 | 较简单 | 稍复杂 |
| 栈溢出风险 | 有 | 无 |
| 访问顺序 | 深度优先 | 层级扩展 |
在实际应用中,当矩阵规模超过200×200时,建议优先使用BFS实现以避免递归深度过大。
4. 算法优化与边界处理
4.1 并查集(Union-Find)解决方案
对于需要频繁查询连通性的场景,并查集是更优的选择:
python复制class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
def find(self, x):
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]] # 路径压缩
x = self.parent[x]
return x
def union(self, x, y):
rootx = self.find(x)
rooty = self.find(y)
if rootx != rooty:
self.parent[rootx] = rooty
def solve(board):
if not board:
return
rows, cols = len(board), len(board[0])
uf = UnionFind(rows * cols + 1) # 额外一个虚拟节点用于边界连接
dummy = rows * cols
for i in range(rows):
for j in range(cols):
if board[i][j] == 'O':
if i == 0 or i == rows-1 or j == 0 or j == cols-1:
uf.union(i*cols + j, dummy)
else:
for di, dj in [(-1,0),(1,0),(0,-1),(0,1)]:
ni, nj = i+di, j+dj
if 0 <= ni < rows and 0 <= nj < cols and board[ni][nj] == 'O':
uf.union(i*cols + j, ni*cols + nj)
for i in range(rows):
for j in range(cols):
if uf.find(i*cols + j) != uf.find(dummy):
board[i][j] = 'X'
4.2 复杂度分析
| 算法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| DFS | O(mn) | O(mn) |
| BFS | O(mn) | O(min(m,n)) |
| 并查集 | O(mnα(mn)) | O(mn) |
其中α是反阿克曼函数,在实际应用中可视为常数。
5. 常见错误与调试技巧
5.1 典型错误案例
-
边界条件遗漏:
- 忘记处理空矩阵输入
- 只检查了四条边中的某几条
-
标记冲突:
- 使用与原始数据相同的标记导致混淆
- 没有在最后正确恢复标记区域
-
遍历方向不全:
- 只考虑了上下或左右两个方向
- 对角线方向的错误处理
5.2 调试建议
- 小规模测试用例:
python复制[["X","X","X","X"],
["X","O","O","X"],
["X","X","O","X"],
["X","O","X","X"]]
- 边界测试用例:
python复制[] # 空矩阵
[["X"]] # 单元素
[["O","O"],["O","O"]] # 全O矩阵
- 打印中间状态:
python复制def print_board(board):
for row in board:
print(' '.join(row))
print('-'*10)
5.3 性能优化技巧
-
提前终止条件:
- 当矩阵尺寸小于3×3时,不可能有被包围的区域
- 如果边界没有'O',可以直接替换所有'O'
-
迭代式DFS:
python复制def dfs_iterative(board, i, j):
stack = [(i,j)]
while stack:
i, j = stack.pop()
if 0 <= i < len(board) and 0 <= j < len(board[0]) and board[i][j] == 'O':
board[i][j] = '#'
stack.append((i+1,j))
stack.append((i-1,j))
stack.append((i,j+1))
stack.append((i,j-1))
- 方向数组优化:
python复制directions = [(-1,0),(1,0),(0,-1),(0,1)] # 统一管理方向向量
在实际面试中,建议先写出基础DFS/BFS实现,然后讨论优化方案,最后视情况实现更高级的算法如并查集。这种循序渐进的方式能更好展示问题解决能力。