LeetCode刷题笔记-岛屿问题
LeetCode中有许多岛屿类问题,如以下题目:
这些题目基本上都是给一个二维数组作为网格,0为海洋1为陆地,然后要你求诸如岛屿最大数量面积周长等问题。
接下来讨论一下此类问题的一个通用解法-DFS。
以LeetCode200. 岛屿数量 为例:
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 :输入:grid = [ [“1”,“1”,“1”,“1”,“0”], [“1”,“1”,“0”,“1”,“0”],
[“1”,“1”,“0”,“0”,“0”], [“0”,“0”,“0”,“0”,“0”] ] 输出:1
很容易有如下想法:首先判断一个格子是否为陆地,若是则向上下左右搜索,直到碰到海洋为止,所有搜索到的陆地就是该岛屿的组成部分。
那么该怎么搜索呢?
在二叉树中的搜索可以采用dfs方法,岛屿问题可以看成在一棵四叉树中进行dfs搜索,大体思路与二叉树中的dfs相同,
很容易写出以下代码
void dfs(char[][]grid, int i, int j) {
//朝上下左右四个方向搜索
if(grid[i][j] == '1') {
dfs(grid, i + 1, j);
dfs(grid, i - 1, j);
dfs(grid, i, j + 1);
dfs(grid, i, j - 1);
}
}
二叉树中搜索停止的条件是当前节点为null,在网格中停止的条件也类似,考虑到网格有边界,因此可以把网格边界的格子看成二叉树的叶节点,对上面的dfs进行修改
void dfs(char[][]grid, int i, int j) {
int m = grid.length;
int n = grid[0].length;
if(i >= m || i < 0 || j >= n || j < 0) {
//若超出边界,则停止继续搜索并返回
return;
}
if(grid[i][j] == '1') {
dfs(grid, i + 1, j);
dfs(grid, i - 1, j);
dfs(grid, i, j + 1);
dfs(grid, i, j - 1);
}
}
然而这样还有一个问题,遍历的时候会访问到已搜索过的节点,二叉树中通常使用一个visit[]数组来标记节点是否遍历过,在此由于不需要访问节点的值,因此直接把搜索过的节点值设置为2,当访问到节点值为2的点说明该点已经是某个岛屿的一部分了,就将其跳过
最终的dfs算法如下
void dfs(char[][]grid, int i, int j) {
int m = grid.length;
int n = grid[0].length;
if(i >= m || i < 0 || j >= n || j < 0) {
return;
}
if(grid[i][j] == '1') {
grid[i][j] = '2';
dfs(grid, i + 1, j);
dfs(grid, i - 1, j);
dfs(grid, i, j + 1);
dfs(grid, i, j - 1);
}
}
接下来问题就很好解决了,遍历网格中的点,若为陆地则以该陆地为起点进行dfs搜索,把搜索到的点的值改为2,完成一次从值为1的点开始的搜索说明找到了一个新的岛屿,将岛屿数量加1,最后count值即为所求
完整代码如下
class Solution {
void dfs(char[][]grid, int i, int j) {
int m = grid.length;
int n = grid[0].length;
if(i >= m || i < 0 || j >= n || j < 0) {
return;
}
if(grid[i][j] == '1') {
grid[i][j] = '2';
dfs(grid, i + 1, j);
dfs(grid, i - 1, j);
dfs(grid, i, j + 1);
dfs(grid, i, j - 1);
}
}
public int numIslands(char[][] grid) {
int m = grid.length;
int n = grid[0].length;
int count = 0;
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
if(grid[i][j] == '1') {
count++;
dfs(grid, i, j);
}
}
}
return count;
}
}
接着看下一题
LeetCode463. 岛屿的周长
给定一个 row x col 的二维网格地图 grid ,其中:grid[i][j] = 1 表示陆地, grid[i][j] = 0
表示水域。网格中的格子 水平和垂直
方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100
。计算这个岛屿的周长。
示例:
输入:grid = [[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]]
输出:16
解释:它的周长是上面图片中的 16 个黄色的边
这题可以用简单的遍历解决,但在此我们用dfs解法做。
首先分析岛屿周长但组成,不难发现,岛屿的周长就是岛屿最边缘块的边长之和,岛屿边缘块也就是dfs时返回的块,包括遇到边界和遇到海洋两种情况,由此可对之前的dfs稍加修改:
int dfs(int[][]grid, int i, int j) {
int m = grid.length;
int n = grid[0].length;
if(i >= m || i < 0 || j >= n || j < 0 || grid[i][j] == 0) {
//若下一个方向的块是边界或海洋,则这个方向的边是边界的一部分
return 1;
}
if(grid[i][j] == 1) {
grid[i][j] = 2;
return
dfs(grid, i + 1, j)+
dfs(grid, i - 1, j) +
dfs(grid, i, j + 1) +
dfs(grid, i, j - 1);
}
}
总的代码如下:
class Solution {
int dfs(int[][]grid, int i, int j) {
int m = grid.length;
int n = grid[0].length;
if(i >= m || i < 0 || j >= n || j < 0 || grid[i][j] == 0) {
//若下一个方向的块是边界或海洋,则这个方向的边是边界的一部分
return 1;
}
if(grid[i][j] == 1) {
grid[i][j] = 2;
return dfs(grid, i + 1, j) +
dfs(grid, i - 1, j) +
dfs(grid, i, j + 1) +
dfs(grid, i, j - 1);
}
return 0;
}
public int islandPerimeter(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
if(grid[i][j] == 1) {
return dfs(grid, i, j);
}
}
}
return 0;
}
}
接着看下一题:LeetCode 695. 岛屿的最大面积
给定一个包含了一些 0 和 1 的非空二维数组 grid 。
一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid
的四个边缘都被 0(代表水)包围着。找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为 0 。)
示例 1:
[[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 。
有了前两题的经验这题就很简单了,搜索的时候每搜索到一块陆地就增加岛屿面积,最后返回所有的岛屿中面积最大值即可
参考代码如下:
class Solution {
int dfs(int[][]grid, int i, int j) {
int m = grid.length;
int n = grid[0].length;
if(i >= m || i < 0 || j >= n || j < 0 || grid[i][j] == 0) {
//若下一个方向的块是边界或海洋,则这个方向的边是边界的一部分
return 0;
}
if(grid[i][j] == 1) {
grid[i][j] = 2;
return dfs(grid, i + 1, j) +
dfs(grid, i - 1, j) +
dfs(grid, i, j + 1) +
dfs(grid, i, j - 1) + 1;
}
return 0;
}
public int maxAreaOfIsland(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
int max = 0;
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
if(grid[i][j] == 1) {
max = Math.max(max, dfs(grid, i, j));
}
}
}
return max;
}
}
最后一题是LeetCode 827. 最大人工岛
先来看一下题目描述: 在二维地图上, 0代表海洋, 1代表陆地,我们最多只能将一格 0 海洋变成 1变成陆地。
进行填海之后,地图上最大的岛屿面积是多少?(上、下、左、右四个方向相连的 1 可形成岛屿)
示例 1:
输入: [[1, 0], [0, 1]] 输出: 3
解释: 将一格0变成1,最终连通两个小岛得到面积为 3 的岛屿。示例 2:
输入: [[1, 1], [1, 0]] 输出: 4
解释: 将一格0变成1,岛屿的面积扩大为 4。
这题乍一看和之前的几题都不一样,但实际上还是能用相同的方法解决
首先自然要求出每块岛屿的面积,这与上一题相同,很容易解决
//dfs1用来求各个岛屿的面积
int dfs1(int[][]grid, int i, int j) {
int m = grid.length;
int n = grid[0].length;
if(i >= m || i < 0 || j >= n || j < 0 || grid[i][j] == 0) {
//若下一个方向的块是边界或海洋,则这个方向的边是边界的一部分
return 0;
}
if(grid[i][j] == 1) {
grid[i][j] = 2;
return dfs1(grid, i + 1, j) +
dfs1(grid, i - 1, j) +
dfs1(grid, i, j + 1) +
dfs1(grid, i, j - 1) + 1;
}
return 0;
}
接下来的问题是连接哪两块岛屿才能使得总面积最大,从陆地下手不太好判断相隔一块海洋的两个岛屿,因此这次从海洋下手,对海洋进行dfs,若一块岛屿四个方向有两块是岛屿,则计算他们的面积和,取最大值即可。
此处的难点在于如何获取相邻岛屿的面积,一种想法是给该岛屿的每块标上面积,但这种方法无法区分一个岛屿的两个相隔块,同时因为岛屿的面积是在搜索中动态更新的,给搜索过的点赋值也是一个麻烦的问题。
因此考虑设计一个面积数组,用来保存各块岛屿的面积,
int getArea(int[][]grid, int i, int j) {
int m = grid.length;
int n = grid[0].length;
if(i >= m || i < 0 || j >= n || j < 0) {
return 0;
}
//index里存的是相邻四个块的类型索引,边界和海洋都为0,岛屿则为对应岛屿编号
Set<Integer> index = new HashSet<>();//用set过滤属于同一岛屿的部分,
if(grid[i][j] == 0) {
index.add(i < m - 1?grid[i + 1][j]:0);
index.add(i > 0?grid[i - 1][j]:0);
index.add(j < n - 1?grid[i][j + 1]:0);
index.add(j > 0?grid[i][j - 1]:0);
int total = 0;
for(int k :index) {
if(k != 0) {
total += area.get(k - 2);//0和1被占用,岛屿编号从2开始
}
}
return total + 1;
}
return 0;
}
总的代码如下:
class Solution {
List<Integer> area = new ArrayList<>();
int dfs1(int[][]grid, int i, int j, int index) {
int m = grid.length;
int n = grid[0].length;
if(i >= m || i < 0 || j >= n || j < 0 || grid[i][j] == 0) {
return 0;
}
if(grid[i][j] == 1) {
grid[i][j] = index;
return dfs1(grid, i + 1, j, index) +
dfs1(grid, i - 1, j, index) +
dfs1(grid, i, j + 1, index) +
dfs1(grid, i, j - 1, index) + 1;
}
return 0;
}
int getArea(int[][]grid, int i, int j) {
int m = grid.length;
int n = grid[0].length;
if(i >= m || i < 0 || j >= n || j < 0) {
return 0;
}
Set<Integer> index = new HashSet<>();
if(grid[i][j] == 0) {
index.add(i < m - 1?grid[i + 1][j]:0);
index.add(i > 0?grid[i - 1][j]:0);
index.add(j < n - 1?grid[i][j + 1]:0);
index.add(j > 0?grid[i][j - 1]:0);
int total = 0;
for(int k :index) {
if(k != 0) {
total += area.get(k - 2);
}
}
return total + 1;
}
return 0;
}
public int largestIsland(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
int max = 0;
int index = 2;
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
if(grid[i][j] == 1) {
area.add(dfs1(grid, i, j, index));
index++;
}
}
}
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
if(grid[i][j] == 0) {
max = Math.max(max, getArea(grid, i, j));
}
}
}
return max == 0?m * n:max;
}
}