1. 题目概述与核心思路
LeetCode 130题"Surrounded Regions"是一道经典的矩阵遍历问题,题目要求我们将被'X'完全包围的'O'区域全部替换为'X'。这道题在2023年频繁出现在各大公司的面试中,特别是亚马逊和微软的算法轮次。
关键规则说明:
- 任何边界上的'O'都不会被翻转
- 仅当'O'区域被'X'四面(上下左右)完全包围时才需要转换
- 转换操作需要原地(in-place)完成
我第一次做这道题时,尝试用朴素的DFS逐个检查每个'O'区域是否被包围,结果在200x200的测试用例上超时。后来发现需要利用逆向思维:与其找该被包围的区域,不如先标记不该被包围的区域。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 逆向思维解法详解
2.1 从边界出发的DFS/BFS
正确的解法应该从边界上的'O'出发,标记所有与之相连的'O'。这些就是最终不会被转换的区域。具体步骤:
- 遍历四条边界,遇到'O'就启动DFS/BFS
- 将所有连通的'O'标记为特殊字符(如'T')
- 遍历整个矩阵,将剩余的'O'转为'X'
- 将所有'T'恢复为'O'
Python实现示例:
python复制def solve(board):
if not board: return
rows, cols = len(board), len(board[0])
def dfs(r, c):
if r<0 or c<0 or r>=rows or c>=cols or board[r][c] != 'O':
return
board[r][c] = 'T'
dfs(r+1, c)
dfs(r-1, c)
dfs(r, c+1)
dfs(r, c-1)
# 遍历边界
for r in range(rows):
dfs(r, 0)
dfs(r, cols-1)
for c in range(cols):
dfs(0, c)
dfs(rows-1, c)
# 转换矩阵
for r in range(rows):
for c in range(cols):
if board[r][c] == 'O':
board[r][c] = 'X'
elif board[r][c] == 'T':
board[r][c] = 'O'
2.2 复杂度分析
- 时间复杂度:O(M×N),每个节点最多被访问两次(标记和恢复)
- 空间复杂度:O(M×N),DFS的递归栈在最坏情况下可能需要这么多空间
提示:当处理超大矩阵时,BFS的迭代实现通常比DFS递归更安全,可以避免栈溢出问题。
3. 算法优化与变种
3.1 并查集(Union-Find)解法
虽然DFS/BFS是最直观的解法,但面试官可能会要求用并查集实现。基本思路:
- 虚拟一个dummy节点作为所有边界'O'的父节点
- 遍历矩阵,将'O'节点与相邻节点union
- 最后检查每个'O'是否与dummy节点连通
Python实现关键部分:
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 r in range(rows):
for c in range(cols):
if board[r][c] == 'O':
if r == 0 or r == rows-1 or c == 0 or c == cols-1:
uf.union(r * cols + c, dummy)
else:
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
nr, nc = r + dr, c + dc
if 0<=nr<rows and 0<=nc<cols and board[nr][nc] == 'O':
uf.union(r * cols + c, nr * cols + nc)
for r in range(rows):
for c in range(cols):
if uf.find(r * cols + c) != uf.find(dummy):
board[r][c] = 'X'
3.2 实际性能对比
在LeetCode测试用例上的实测数据:
| 方法 | 执行时间(ms) | 内存消耗(MB) |
|---|---|---|
| DFS | 120-140 | 15-18 |
| BFS | 130-150 | 14-16 |
| 并查集 | 180-220 | 19-22 |
虽然并查集理论复杂度更好,但由于常数因子较大,在小规模数据上反而不如DFS/BFS高效。但在需要频繁连通性检查的动态场景中,并查集优势明显。
4. 常见错误与调试技巧
4.1 典型错误案例
- 忘记处理空输入:直接开始遍历会导致IndexError
- 边界条件错误:只检查了左边界和上边界,漏掉右边和下边
- 原地修改问题:在标记阶段错误地直接改为'X',导致后续遍历出错
- 方向数组遗漏:DFS中漏掉某个方向(常见的是忘记向左搜索)
4.2 调试建议
当你的解法无法通过所有测试用例时:
- 先测试1x1矩阵:['O']应该保持不变
- 测试全'O'矩阵:边界应该保留,内部可能被转换
- 打印中间状态:在标记阶段后输出矩阵,检查'T'的位置是否正确
- 可视化遍历过程:对于小矩阵,可以手动绘制DFS/BFS的访问顺序
注意:LeetCode的在线judge有时会显示错误的预期输出。遇到疑似错误时,先仔细阅读题目描述,确认理解无误。
5. 面试扩展问题
面试官可能会基于此题提出以下变种问题:
-
如果允许对角线包围怎么办?(8方向连通性)
- 解法:调整方向数组,包含8个方向
-
如果要求找出所有被包围的区域数量?
- 解法:在最终转换前统计连续'O'区域
-
如果矩阵非常大无法放入内存?
- 解法:使用外部排序+分块处理,但面试中通常不需要实现细节
-
并行算法如何设计?
- 思路:将矩阵分块,各线程处理不同边界区域,最后合并结果
6. 刷题心得与进阶建议
这道题教会我们一个重要的算法思维模式:当直接解决问题困难时,可以尝试逆向思考。类似思路的问题还有:
- LeetCode 200. Number of Islands
- LeetCode 994. Rotting Oranges
- LeetCode 529. Minesweeper
在实际刷题中,我建议:
- 先尝试自己思考30分钟,不要直接看答案
- 写出暴力解法,再思考优化方向
- 对于图论问题,多在纸上画出遍历过程
- 整理错题本,记录每种错误类型
最后分享一个效率技巧:使用collections.deque实现BFS时,popleft()比list的pop(0)快5倍以上。对于大规模矩阵,这个优化可能决定是否通过时间限制。
