LeetCode算法日记 - Day 64: 岛屿的最大面积、被围绕的区域
目录
1. 岛屿的最大面积
1.1 题目解析
1.2 解法
1.3 代码实现
2. 被围绕的区域
2.1 题目解析
2.2 解法
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]
为0
或1
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 代码实现
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 代码实现
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')