LeetCode算法日记 - Day 64: 岛屿的最大面积、被围绕的区域

目录

[1. 岛屿的最大面积](#1. 岛屿的最大面积)

[1.1 题目解析](#1.1 题目解析)

[1.2 解法](#1.2 解法)

[1.3 代码实现](#1.3 代码实现)

[2. 被围绕的区域](#2. 被围绕的区域)

[2.1 题目解析](#2.1 题目解析)

[2.2 解法](#2.2 解法)

[2.3 代码实现](#2.3 代码实现)


1. 岛屿的最大面积

https://leetcode.cn/problems/max-area-of-island/

给你一个大小为 m x n 的二进制矩阵 grid

岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。

岛屿的面积是岛上值为 1 的单元格的数目。

计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0

示例 1:

复制代码
输入:grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,1,1,0,1,0,0,0,0,0,0,0,0],[0,1,0,0,1,1,0,0,1,0,1,0,0],[0,1,0,0,1,1,0,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,1,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,0,0,0,1,1,0,0,0,0]]
输出:6
解释:答案不应该是 11 ,因为岛屿只能包含水平或垂直这四个方向上的 1 。

示例 2:

复制代码
输入:grid = [[0,0,0,0,0,0,0,0]]
输出:0

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 50
  • grid[i][j]01

1.1 题目解析

题目本质

这是一道经典的「连通分量统计」问题,核心是在二维网格中找出所有由 1 组成的连通区域,并返回最大连通区域的面积。

常规解法

遍历矩阵,遇到未访问的 1 就开始搜索,统计这块岛屿的面积,最后返回所有岛屿中的最大值。

问题分析

直观解法就是正解,关键在于选择合适的搜索方式。可以用 DFS(深度优先搜索)或 BFS(广度优先搜索),两者时间复杂度相同,只是实现方式不同:

  • DFS 用递归,代码简洁但可能栈溢出
  • BFS 用队列,需要额外空间但更安全

思路转折

要想避免重复统计 → 必须标记已访问节点 → 使用 vis 数组。要想统计完整岛屿 → 必须遍历所有相邻的 1 → 四个方向递归/迭代搜索。

1.2 解法

算法思想

  • 使用 DFS(深度优先搜索) 遍历每个岛屿,统计岛屿面积

  • 维护 vis 数组标记已访问节点,避免重复计算

  • 递推逻辑:当前岛屿面积 = 1(当前节点)+ 四个方向未访问邻居的岛屿面积之和

  • 公式:area = 1 + ∑ area(neighbor),其中 neighbor 是四个方向上满足条件的邻居

**i)初始化:**记录矩阵大小 m×n,创建 vis 数组标记访问状态

**ii)双层遍历:**遍历矩阵每个位置 (i,j)

**iii)触发搜索:**若 grid[i][j] == 1 且未访问,则从该点开始 DFS 搜索

iv)DFS 递归:

  • 标记当前节点为已访问
  • 面积计数器 +1
  • 向四个方向递归搜索未访问的 1
  • 返回累计面积

**v)更新答案:**每次 DFS 返回后,用返回值更新最大面积

**vi)返回结果:**遍历结束后返回最大岛屿面积

易错点

  • **提前标记:**必须在递归调用前标记 vis[x][y] = true,否则同一节点可能被重复访问导致死循环或计数错误
  • **全局变量陷阱:**使用全局变量 cur 统计面积时,每次新岛屿搜索前必须重置为 0,否则会累加到之前的岛屿面积
  • **边界检查顺序:**必须先检查边界 x>=0 && x<m && y>=0 && y<n,再访问 grid[x][y],否则可能数组越界

1.3 代码实现

java 复制代码
class Solution {
    int m, n;
    boolean[][] vis;
    int[] dx = {0, 0, 1, -1};
    int[] dy = {1, -1, 0, 0};
    int cur;  // 当前岛屿面积
    
    public int maxAreaOfIsland(int[][] grid) {
        m = grid.length;
        if (m == 0) return 0;
        n = grid[0].length;
        vis = new boolean[m][n];
        
        int maxArea = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1 && !vis[i][j]) {
                    cur = 0;  // 重置计数器
                    int area = dfs(grid, i, j);
                    maxArea = Math.max(maxArea, area);
                }
            }
        }
        return maxArea;
    }
    
    private int dfs(int[][] grid, int x, int y) {
        vis[x][y] = true;
        cur++;  // 统计当前节点
        
        // 四个方向递归
        for (int i = 0; i < 4; i++) {
            int nx = x + dx[i];
            int ny = y + dy[i];
            if (nx >= 0 && nx < m && ny >= 0 && ny < n 
                && grid[nx][ny] == 1 && !vis[nx][ny]) {
                vis[nx][ny] = true;  // 提前标记
                dfs(grid, nx, ny);
            }
        }
        return cur;
    }
}

复杂度分析

  • 时间复杂度:O(m × n),每个节点最多访问一次
  • 空间复杂度:O(m × n),vis 数组占用 O(m × n) 空间,递归栈最坏情况(整个矩阵都是 1)占用 O(m × n) 空间

2. 被围绕的区域

给你一个 m x n 的矩阵 board ,由若干字符 'X''O' 组成,捕获 所有 被围绕的区域

  • **连接:**一个单元格与水平或垂直方向上相邻的单元格连接。
  • 区域:连接所有 'O' 的单元格来形成一个区域。
  • 围绕: 如果您可以用 'X' 单元格 连接这个区域 ,并且区域中没有任何单元格位于 board 边缘,则该区域被 'X' 单元格围绕。

通过 原地 将输入矩阵中的所有 'O' 替换为 'X'捕获被围绕的区域。你不需要返回任何值。

示例 1:

**输入:**board = [['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']]

解释:

在上图中,底部的区域没有被捕获,因为它在 board 的边缘并且不能被围绕。

示例 2:

**输入:**board = [['X']]

输出:[['X']]

提示:

  • m == board.length
  • n == board[i].length
  • 1 <= m, n <= 200
  • board[i][j]'X''O'

2.1 题目解析

题目本质

这是一道「反向标记连通分量」问题,核心是区分哪些 'O' 区域是被完全围绕的(内部),哪些是连通到边界的(外部)。

常规解法

遍历矩阵内部的每个 'O',对其进行 DFS/BFS 搜索,检查这个连通区域是否能到达边界。如果不能到达边界,就把整个区域标记为 'X'。

问题分析

这种"逐个检查是否到边界"的做法存在大量重复搜索。同一个连通区域内的每个 'O' 都会触发一次搜索,时间复杂度可能达到 O((m×n)²)。当矩阵较大且 'O' 较多时,效率极低。

思路转折

要想避免重复搜索 → 必须换个角度思考 → 反向标记。关键洞察:不被围绕的 'O' 一定连通到边界 → 从边界出发标记所有连通的 'O' → 剩下未标记的 'O' 就是被围绕的 → 直接替换为 'X'。这样每个节点只访问一次,时间复杂度降到 O(m×n)。

2.2 解法

算法思想

  • 使用 反向标记 + DFS/BFS 的策略:先保护边界连通的 'O',再消灭内部的 'O'

  • 三步走:

    • 标记阶段:从四条边出发,把所有连通的 'O' 临时标记为 'A'(表示安全区域)

    • 替换阶段:遍历矩阵,将剩余的 'O'(被围绕的)改为 'X',将 'A' 还原为 'O'

  • 核心逻辑:安全区域 = 从边界可达的 'O',被围绕区域 = 所有 'O' - 安全区域

**i)边界检查:**处理空矩阵边界情况,初始化矩阵大小 m×n

ii)第一轮标记 (保护边界连通区域)

  • 遍历上下两条边的每一列,若为 'O' 则 DFS 标记为 'A'

  • 遍历左右两条边的每一行(跳过四个角避免重复),若为 'O' 则 DFS 标记为 'A'

iii)DFS 递归标记:

  • 将当前 'O' 改为 'A'

  • 向四个方向递归,继续标记相邻的 'O'

iv)第二轮替换(处理结果):

  • 遍历整个矩阵

  • 遇到 'O' → 改为 'X'(被围绕的区域)

  • 遇到 'A' → 改为 'O'(恢复边界连通区域)

**v)原地修改:**直接在原矩阵上操作,无需返回值

易错点

  • **边界遍历重复:**遍历左右两条边时,要从 i=1 到 i=m-2,避免重复处理四个角的位置(它们已经在上下边的遍历中处理过)
  • **标记时机错误:**必须在访问节点时立即标记为 'A',不能先入队/递归再标记,否则同一节点可能被多次访问导致栈溢出或性能问题
  • **替换逻辑混乱:**第二轮遍历时,'O' 和 'A' 的处理不能搞反:'O' 是要消灭的(改为 'X'),'A' 是要保留的(还原为 'O')
  • **临时标记冲突:**选择 'A' 作为临时标记是因为题目只有 'X' 和 'O',若题目包含其他字符需要选择不冲突的标记

2.3 代码实现

java 复制代码
class Solution {
    int m, n;
    int[] dx = {0, 0, 1, -1};
    int[] dy = {1, -1, 0, 0};

    public void solve(char[][] board) {
        if (board == null || board.length == 0) return;
        m = board.length;
        n = board[0].length;

        // 1. 从四条边出发,标记所有连通的 'O' 为 'A'
        // 上下两条边
        for (int j = 0; j < n; j++) {
            if (board[0][j] == 'O') dfs(board, 0, j);
            if (board[m - 1][j] == 'O') dfs(board, m - 1, j);
        }
        // 左右两条边(跳过角落避免重复)
        for (int i = 1; i < m - 1; i++) {
            if (board[i][0] == 'O') dfs(board, i, 0);
            if (board[i][n - 1] == 'O') dfs(board, i, n - 1);
        }

        // 2. 遍历矩阵,'O' 改为 'X','A' 还原为 '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] == 'A') {
                    board[i][j] = 'O';      // 边界连通区域
                }
            }
        }
    }

    // DFS:把从 (x, y) 出发连通的 'O' 标记为 'A'
    private void dfs(char[][] board, int x, int y) {
        board[x][y] = 'A';  // 立即标记
        
        for (int k = 0; k < 4; k++) {
            int nx = x + dx[k];
            int ny = y + dy[k];
            if (nx >= 0 && nx < m && ny >= 0 && ny < n && board[nx][ny] == 'O') {
                dfs(board, nx, ny);
            }
        }
    }
}

复杂度分析

  • **时间复杂度:O(m × n),**每个节点最多访问一次(标记阶段一次 + 替换阶段一次)
  • **空间复杂度:DFS 版本:O(m × n),**最坏情况递归栈深度(整个矩阵都是边界连通的 'O')
相关推荐
Christo33 小时前
关于K-means和FCM的凸性问题讨论
人工智能·算法·机器学习·数据挖掘·kmeans
Lisonseekpan3 小时前
Spring Boot 中使用 Caffeine 缓存详解与案例
java·spring boot·后端·spring·缓存
为java加瓦3 小时前
Rust 的类型自动解引用:隐藏在人体工学设计中的魔法
java·服务器·rust
SimonKing3 小时前
分布式日志排查太头疼?TLog 让你一眼看穿请求链路!
java·后端·程序员
消失的旧时光-19433 小时前
Kotlin 判空写法对比与最佳实践
android·java·kotlin
_不会dp不改名_3 小时前
leetcode_1382 将二叉搜索树变平衡树
算法·leetcode·职场和发展
小许学java3 小时前
Spring AI快速入门以及项目的创建
java·开发语言·人工智能·后端·spring·ai编程·spring ai
一叶飘零_sweeeet4 小时前
从 “死锁“ 到 “解耦“:重构中间服务破解 Java 循环依赖难题
java·循环依赖
greentea_20134 小时前
Codeforces Round 173 B. Digits(2043)
c++·算法