LeetCode 130. 被围绕的区域
问题描述
给定一个 m x n
的矩阵 board
,由字符 'X'
和 'O'
组成。需要找到所有被 'X'
完全围绕的区域,并将这些区域中的所有 'O'
替换为 'X'
。边界上的 'O'
不会被围绕,与边界相连的 'O'
也不会被替换。
示例:
X X X X
X O O X
X X O X
X O X X
输出:
X X X X
X X X X
X X X X
X O X X
算法思路
核心思路:
所有与边界相连的 'O'
都不会被替换:
- 从边界上的每个
'O'
出发,标记所有与其相连的'O'
(这些'O'
不会被替换) - 遍历整个矩阵:
- 将未被标记的
'O'
(被围绕的)替换为'X'
- 恢复被标记的
'O'
- 将未被标记的
两种实现方法:
- 深度优先搜索 (DFS):递归标记边界相连区域
- 广度优先搜索 (BFS):队列迭代标记边界相连区域
代码实现
方法一:DFS(递归实现)
class Solution {
public void solve(char[][] board) {
if (board == null || board.length == 0) return;
int m = board.length;
int n = board[0].length;
// 步骤1:标记所有与边界相连的 'O'(使用 DFS)
// 遍历左右边界(第一列和最后一列)
for (int i = 0; i < m; i++) {
if (board[i][0] == 'O') {
dfs(board, i, 0); // 从左侧边界开始DFS
}
if (board[i][n - 1] == 'O') {
dfs(board, i, n - 1); // 从右侧边界开始DFS
}
}
// 遍历上下边界(第一行和最后一行)
for (int j = 0; j < n; j++) {
if (board[0][j] == 'O') {
dfs(board, 0, j); // 从顶部边界开始DFS
}
if (board[m - 1][j] == 'O') {
dfs(board, m - 1, j); // 从底部边界开始DFS
}
}
// 步骤2:遍历整个矩阵,替换被围绕的 'O',恢复标记的 '#'
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == 'O') {
board[i][j] = 'X'; // 被围绕的区域
} else if (board[i][j] == '#') {
board[i][j] = 'O'; // 恢复边界相连区域
}
}
}
}
// DFS 辅助函数:标记所有与 (i, j) 相连的 'O'
private void dfs(char[][] board, int i, int j) {
// 越界检查或遇到非 'O' 时返回
if (i < 0 || i >= board.length || j < 0 || j >= board[0].length || 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); // 右
}
}
方法二:BFS(队列实现)
import java.util.LinkedList;
import java.util.Queue;
class Solution {
public void solve(char[][] board) {
if (board == null || board.length == 0) return;
int m = board.length;
int n = board[0].length;
Queue<int[]> queue = new LinkedList<>(); // 存储待处理的坐标
int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; // 方向数组:上、下、左、右
// 步骤1:将边界上的 'O' 加入队列并标记
// 左右边界
for (int i = 0; i < m; i++) {
if (board[i][0] == 'O') {
queue.offer(new int[]{i, 0});
board[i][0] = '#'; // 标记
}
if (board[i][n - 1] == 'O') {
queue.offer(new int[]{i, n - 1});
board[i][n - 1] = '#';
}
}
// 上下边界
for (int j = 0; j < n; j++) {
if (board[0][j] == 'O') {
queue.offer(new int[]{0, j});
board[0][j] = '#';
}
if (board[m - 1][j] == 'O') {
queue.offer(new int[]{m - 1, j});
board[m - 1][j] = '#';
}
}
// BFS 扩展标记
while (!queue.isEmpty()) {
int[] cell = queue.poll();
int i = cell[0], j = cell[1];
for (int[] dir : dirs) {
int ni = i + dir[0]; // 新行坐标
int nj = j + dir[1]; // 新列坐标
// 检查新坐标是否合法且为 'O'
if (ni >= 0 && ni < m && nj >= 0 && nj < n && board[ni][nj] == 'O') {
board[ni][nj] = '#'; // 标记
queue.offer(new int[]{ni, nj}); // 加入队列
}
}
}
// 步骤2:替换被围绕的 'O',恢复标记的 '#'
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == 'O') {
board[i][j] = 'X'; // 被围绕的区域
} else if (board[i][j] == '#') {
board[i][j] = 'O'; // 恢复边界相连区域
}
}
}
}
}
算法分析
- 时间复杂度:O(m × n)
每个单元格最多被访问两次(标记阶段和替换阶段)。 - 空间复杂度:
- DFS:O(m × n)(递归调用栈的深度,最坏情况是整个矩阵)
- BFS:O(m × n)(队列存储所有边界相连的
'O'
)
算法过程
输入矩阵:
初始状态:
X X X X
X O O X
X X O X
X O X X
步骤1:标记边界相连的 'O'
- 发现边界
'O'
位置:(3,1)
(第4行第2列) - 从
(3,1)
开始 DFS/BFS:- 标记
(3,1)
为#
- 检查相邻位置(均为
'X'
或越界),无扩展
- 标记
标记后矩阵:
X X X X
X O O X
X X O X
X # X X // (3,1) 被标记
步骤2:替换与恢复
- 遍历所有单元格:
- 未被标记的
'O'
(位置(1,1)
,(1,2)
,(2,2)
)→ 替换为'X'
- 标记的
'#'
(位置(3,1)
)→ 恢复为'O'
- 未被标记的
最终结果:
X X X X
X X X X // (1,1) 和 (1,2) 被替换
X X X X // (2,2) 被替换
X O X X // (3,1) 恢复为 'O'
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
char[][] board1 = {
{'X','X','X','X'},
{'X','O','O','X'},
{'X','X','O','X'},
{'X','O','X','X'}
};
solution.solve(board1);
printBoard(board1); // 应输出示例结果
// 测试用例2:全为 'X'
char[][] board2 = {
{'X','X','X'},
{'X','X','X'},
{'X','X','X'}
};
solution.solve(board2);
printBoard(board2); // 矩阵不变
// 测试用例3:全为 'O'
char[][] board3 = {
{'O','O','O','O'},
{'O','O','O','O'},
{'O','O','O','O'}
};
solution.solve(board3);
printBoard(board3); // 全部保留 'O'
// 测试用例4:空矩阵
char[][] board4 = {};
solution.solve(board4); // 不抛出异常
// 测试用例5:单元素矩阵
char[][] board5 = {{'O'}};
solution.solve(board5);
printBoard(board5); // 输出 [['O']]
}
// 辅助函数:打印矩阵
private static void printBoard(char[][] board) {
for (char[] row : board) {
System.out.println(Arrays.toString(row));
}
System.out.println();
}
关键点
-
逆向思维
:
直接找被围绕的区域困难 → 改为标记未被围绕的区域
(与边界相连的'O'
)。 -
标记技巧:
- 使用临时字符
'#'
标记需保留的'O'
- 最后统一替换:未被标记的
'O'
→'X'
,标记的'#'
→'O'
- 使用临时字符
-
边界处理:
- 只从
四条边界
开始搜索(第一行/最后一行/第一列/最后一列) - 边界上的
'O'
必然不会被围绕
- 只从
-
连通性判断:
- DFS:递归探索四个方向(上、下、左、右)
- BFS:队列按层扩展,方向数组简化代码
常见问题
-
为什么 DFS 可能导致栈溢出?
当矩阵极大且全是'O'
时,递归深度可能过大。此时 BFS 更安全。 -
如何处理空矩阵?
在函数开头添加空值检查:if (board == null || board.length == 0) return;
-
为什么标记为
'#'
?
任意非'X'
/'O'
的字符均可,用于区分需保留的'O'
。 -
如何验证正确性?
测试用例需覆盖:- 标准示例
- 全
'X'
/ 全'O'
- 边界
'O'
连通内部 - 单元素矩阵
-
BFS 中为什么先标记再入队?
避免重复访问:在加入队列时立即标记,防止同一位置被多次处理。