1. 问题背景与核心概念
今天我们来聊聊LeetCode上两道经典的图论问题——"被围绕的区域"(编号130)和"岛屿数量"(编号200)。这两道题看似独立,实则都涉及图的遍历算法(DFS/BFS)在矩阵类问题中的应用,是理解二维空间搜索的绝佳案例。
先明确题目要求:
- 被围绕的区域:给定一个包含'X'和'O'的二维矩阵,将所有被'X'完全包围的'O'替换为'X'。边界上的'O'及与其相连的'O'不会被包围。
- 岛屿数量:同样是'1'(陆地)和'0'(水)组成的二维网格,计算其中岛屿的数量。岛屿由水平或垂直相邻的陆地连接形成。
关键理解:这两题本质上都是"连通分量"问题,区别在于处理边界条件的方式不同。被围绕的区域需要特殊处理矩阵边缘的'O',而岛屿数量则对所有'1'一视同仁。
2. 算法选型:DFS与BFS的抉择
2.1 深度优先搜索(DFS)实现
DFS采用递归或栈实现的"一条路走到黑"策略,适合快速探索单条路径。以岛屿数量为例:
python复制def numIslands(grid):
if not grid: return 0
count = 0
rows, cols = len(grid), len(grid[0])
def dfs(r, c):
if r<0 or c<0 or r>=rows or c>=cols or grid[r][c] != '1':
return
grid[r][c] = '0' # 标记为已访问
dfs(r+1, c)
dfs(r-1, c)
dfs(r, c+1)
dfs(r, c-1)
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
count += 1
dfs(r, c)
return count
时间复杂度:O(M×N),每个单元格最多被访问一次
空间复杂度:O(M×N),递归调用栈在最坏情况下可能达到网格大小
2.2 广度优先搜索(BFS)实现
BFS使用队列逐层扩展,适合寻找最短路径。被围绕的区域问题更适合BFS:
python复制from collections import deque
def solve(board):
if not board: return
rows, cols = len(board), len(board[0])
queue = deque()
# 将边界上的'O'入队
for r in range(rows):
for c in [0, cols-1]:
if board[r][c] == 'O':
queue.append((r, c))
board[r][c] = 'E' # 标记为特殊值
for c in range(cols):
for r in [0, rows-1]:
if board[r][c] == 'O':
queue.append((r, c))
board[r][c] = 'E'
# BFS扩展
directions = [(-1,0),(1,0),(0,-1),(0,1)]
while queue:
r, c = queue.popleft()
for dr, dc in directions:
nr, nc = r + dr, c + dc
if 0<=nr<rows and 0<=nc<cols and board[nr][nc] == 'O':
board[nr][nc] = 'E'
queue.append((nr, nc))
# 最终处理
for r in range(rows):
for c in range(cols):
if board[r][c] == 'O':
board[r][c] = 'X'
elif board[r][c] == 'E':
board[r][c] = 'O'
性能对比:
| 算法 | 优势场景 | 劣势场景 |
|---|---|---|
| DFS | 代码简洁,适合连通性检查 | 递归深度大时可能栈溢出 |
| BFS | 适合最短路径,非递归实现更安全 | 需要额外队列空间 |
3. 关键技巧与优化策略
3.1 标记法的艺术
两种常见标记方式:
- 原地修改:如将访问过的'O'改为'E'(被围绕的区域)
- 辅助矩阵:新建visited数组记录访问状态(岛屿数量)
实测建议:在LeetCode环境中,原地修改通常比创建新矩阵快15-20%,但会破坏原始数据。面试时应先与面试官确认需求。
3.2 边界处理模式
被围绕的区域问题有个经典技巧——反向思维:
- 先标记所有边界连通区域
- 再处理内部区域
- 最后恢复边界标记
这比直接寻找被包围区域更高效,因为:
- 边界'O'数量通常远少于内部'O'
- 避免了为每个'O'检查是否被包围的昂贵计算
3.3 方向数组的工程实践
定义方向数组时,我推荐两种写法:
python复制# 写法1:显式列出(推荐)
directions = [(-1,0),(1,0),(0,-1),(0,1)]
# 写法2:使用product生成
from itertools import product
directions = [(dr,dc) for dr,dc in product([-1,0,1],[-1,0,1])
if abs(dr) != abs(dc)] # 排除对角线
避坑指南:
- 确保方向不重复(如不要同时有(1,0)和(1,0))
- 注意边界检查与方向移动的先后顺序
- 对角移动是否允许要根据题意(大多数矩阵题只允许四联通)
4. 常见错误与调试技巧
4.1 栈溢出问题
当网格很大时,DFS递归可能导致栈溢出。解决方法:
- 改用BFS
- 使用迭代式DFS(显式栈)
python复制def dfs_iterative(r, c):
stack = [(r, c)]
while stack:
r, c = stack.pop()
if 0<=r<rows and 0<=c<cols and grid[r][c] == '1':
grid[r][c] = '0'
stack.append((r+1, c))
stack.append((r-1, c))
stack.append((r, c+1))
stack.append((r, c-1))
4.2 访问顺序陷阱
错误的访问顺序会导致重复访问或漏访问。正确做法:
- 标记时机:入队/入栈时立即标记,而非处理时标记
- 检查顺序:先检查边界再访问,避免数组越界
4.3 测试用例设计
必备测试案例:
- 空矩阵
- 全'O'/全'X'矩阵
- 螺旋型分布案例
- 单行/单列特殊情况
例如被围绕的区域应测试:
python复制[
["X","X","X","X"],
["X","O","O","X"],
["X","X","O","X"],
["X","O","X","X"]
]
# 应只保留最右下角的'O'
5. 进阶扩展与相似题型
5.1 并查集解法
对于岛屿数量问题,并查集(Union-Find)是另一种优雅解法:
python复制class UnionFind:
def __init__(self, grid):
rows, cols = len(grid), len(grid[0])
self.count = 0
self.parent = [i for i in range(rows * cols)]
self.rank = [0] * (rows * cols)
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
self.count += 1
def find(self, i):
if self.parent[i] != i:
self.parent[i] = self.find(self.parent[i])
return self.parent[i]
def union(self, x, y):
xroot = self.find(x)
yroot = self.find(y)
if xroot == yroot:
return
if self.rank[xroot] < self.rank[yroot]:
xroot, yroot = yroot, xroot
self.parent[yroot] = xroot
if self.rank[xroot] == self.rank[yroot]:
self.rank[xroot] += 1
self.count -= 1
def numIslands(grid):
if not grid: return 0
rows, cols = len(grid), len(grid[0])
uf = UnionFind(grid)
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
grid[r][c] = '0' # 避免重复处理
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 grid[nr][nc] == '1':
uf.union(r * cols + c, nr * cols + nc)
return uf.count
5.2 相似题型推荐
- 岛屿周长(LeetCode 463)
- 最大岛屿面积(LeetCode 695)
- 统计子岛屿(LeetCode 1905)
- 远离大陆的最远距离(LeetCode 1162)
5.3 实际工程应用
这类算法在以下场景有实际应用:
- 图像处理中的连通区域分析
- 游戏开发中的地图生成与探索
- 社交网络的关联用户发现
- 电路板布线检查
我在处理一个图像分析项目时,就曾用改进的BFS算法来识别文档图像中的文字区块。与标准解法不同,我们还需要处理:
- 动态合并相邻但属性相似的区域
- 设置最小/最大区域阈值
- 处理倾斜连接的情况
这种实际场景的复杂度往往远超LeetCode题目,但核心算法思想是相通的。掌握好这些基础问题的解法,能帮助你在面对更复杂问题时快速找到突破口。
