算法题 被围绕的区域

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' 都不会被替换:

  1. 从边界上的每个 'O' 出发,标记所有与其相连的 'O'(这些 'O' 不会被替换)
  2. 遍历整个矩阵:
    • 将未被标记的 'O'(被围绕的)替换为 'X'
    • 恢复被标记的 'O'

两种实现方法

  1. 深度优先搜索 (DFS):递归标记边界相连区域
  2. 广度优先搜索 (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)
    每个单元格最多被访问两次(标记阶段和替换阶段)。
  • 空间复杂度
    • DFSO(m × n)(递归调用栈的深度,最坏情况是整个矩阵)
    • BFSO(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();
}

关键点

  1. 逆向思维
    直接找被围绕的区域困难 → 改为标记 未被围绕的区域(与边界相连的 'O')。

  2. 标记技巧

    • 使用临时字符 '#' 标记需保留的 'O'
    • 最后统一替换:未被标记的 'O''X',标记的 '#''O'
  3. 边界处理

    • 只从 四条边界 开始搜索(第一行/最后一行/第一列/最后一列)
    • 边界上的 'O' 必然不会被围绕
  4. 连通性判断

    • DFS:递归探索四个方向(上、下、左、右)
    • BFS:队列按层扩展,方向数组简化代码

常见问题

  1. 为什么 DFS 可能导致栈溢出?
    当矩阵极大且全是 'O' 时,递归深度可能过大。此时 BFS 更安全。

  2. 如何处理空矩阵?
    在函数开头添加空值检查:if (board == null || board.length == 0) return;

  3. 为什么标记为 '#'
    任意非 'X'/'O' 的字符均可,用于区分需保留的 'O'

  4. 如何验证正确性?
    测试用例需覆盖:

    • 标准示例
    • 'X' / 全 'O'
    • 边界 'O' 连通内部
    • 单元素矩阵
  5. BFS 中为什么先标记再入队?
    避免重复访问:在加入队列时立即标记,防止同一位置被多次处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值