文章目录

  • ​​LeetCode. 200 岛屿数量​​
  • ​​DFS​​
  • ​​BFS​​
  • ​​并查集​​
  • ​​LeetCode 463. 岛屿周长​​
  • ​​LeetCode 1905. 统计子岛屿数量​​
  • ​​LeetCode 1254. 封闭岛屿数量​​
  • ​​LeetCode 695. 最大岛屿​​
  • ​​LeetCode 827. 最大人工岛​​
  • ​​小结​​
  • ​​待办​​
  • ​​LeetCode 694. 不同岛屿数量​​
  • ​​LeetCode 711. 不同岛屿数量II​​

LeetCode. 200 岛屿数量

给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

DFS

其实就是求连通块的个数。这道题可以通过DFS,BFS,并查集,三种方法来做。

先说比较简单的DFS:

两层循环,对每个为​​1​​​的位置,进行DFS,把跟这个点相连的所有点都访问一遍,并且访问后将该位置置为​​0​​,以便后续不会重复访问。

class Solution {
// 4个方向
int[] dx = {1, -1, 0, 0};
int[] dy = {0, 0, 1, -1};
public int numIslands(char[][] grid) {
int n = grid.length, m = grid[0].length;
int sum = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == '1') {
dfs(grid, i, j);
sum++;
}
}
}
return sum;
}

private void dfs(char[][] grid, int i, int j) {
int n = grid.length, m = grid[0].length;
if (i < 0 || i >= n || j < 0 || j >= m || grid[i][j] == '0') return;
grid[i][j] = '0';
for (int k = 0; k < 4; k++) dfs(grid, i + dx[k], j + dy[k]);
}
}

BFS

上面DFS只是针对某一个点,把这个点所在的整个岛屿给扩展开来了。那么用BFS也能达到同样的效果。

但是BFS有个要注意的点,准备把一个点加入队列时,要先把这个点置为0,再加入队列。

我最开始的写法是:从队列中取出一个点后,再把这个点置为0,提交发现会报超时。这是因为,如果从队列中取出一个点后才把它置为0,则这个点可能由于是其他点的邻接点,而被重复的加入了队列。

class Solution {
int[] dx = {1, -1, 0, 0};
int[] dy = {0, 0, 1, -1};
public int numIslands(char[][] grid) {
int n = grid.length, m = grid[0].length;
int sum = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == '1') {
bfs(grid, i, j);
sum++;
}
}
}
return sum;
}

private void bfs(char[][] grid, int i, int j) {
int n = grid.length, m = grid[0].length;
Queue<Integer> q = new LinkedList<>();
q.offer(i * m + j);
grid[i][j] = '0'; // 加入队列的同时, 置为0
while (!q.isEmpty()) {
int pos = q.poll();
int x = pos / m;
int y = pos % m;
// grid[x][y] = '0'; // 一开始我是在一个点从队列取出后, 才把它置为0, 这种方式会导致相同的点被重复的加入队列
for (int k = 0; k < 4; k++) {
int nx = x + dx[k];
int ny = y + dy[k];
if (nx < 0 || nx >= n || ny < 0 || ny >= m || grid[nx][ny] == '0') continue;
q.offer(nx * m + ny);
grid[nx][ny] = '0'; // 加入队列的同时置为0
}
}
}
}

并查集

由于遍历的顺序是从左到右,从上到下,所以每次合并时,只需要合并当前节点和其右侧,下侧的节点即可。

class Solution {
int[] p;
int num;
public int numIslands(char[][] grid) {
// 并查集解法
int n = grid.length, m = grid[0].length;
// init
p = new int[n * m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == '1') {
int idx = i * m + j;
p[idx] = idx;
num++;
}
}
}

for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == '1') {
if (i + 1 < n && grid[i + 1][j] == '1') union(i * m + j, (i + 1) * m + j);
if (j + 1 < m && grid[i][j + 1] == '1') union(i * m + j, i * m + j + 1);
}
}
}
return num;
}

void union(int x, int y) {
if (find(x) != find(y)) {
p[find(x)] = find(y);
num--; // 合并, 集合减1
}
}

int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
}

LeetCode 岛屿系列问题 463.200.695.827

LeetCode 463. 岛屿周长

这道题是求岛屿周长,标注是Easy,但我觉得可能并不Easy。

同样,我们可以用DFS或BFS来遍历岛屿上的每一块陆地。按照常规的思维,直接计算岛屿周长,可能无从下手。我们可以试着将问题拆分,把大的问题拆分成小的问题。由于一个岛屿是由很多块陆地构成,我们可以考虑对每块陆地进行某种计算,然后组合起来得到整个岛屿的周长。

观察发现,每块陆地都会对整个岛屿的周长有所贡献,每块陆地可能在上下左右4个方向对岛屿周长做出贡献。当一块陆地有x个方向上邻接的都是陆地,那么这块陆地对整个岛屿周长的贡献就是4-x。换个说法,对于一块陆地,其对整个岛屿周长的贡献,是其上下左右4个方向上邻接的是水域边界的数量。

如此以来,思路就比较清晰了。我们通过DFS搜索这个岛屿的全部陆地,每访问一块陆地,就看一下它有几条边是和水域边界相连的,累加每块陆地的贡献,最终得到整个岛屿的周长。

特别的,由于这道题目只有1个岛屿,我们可以不用DFS,而只是简单地遍历每一个小格子,对所有为陆地的格子,计算一下其对周长的贡献即可。

class Solution {
int[] dx = {1, -1, 0, 0};
int[] dy = {0, 0, 1, -1};
public int islandPerimeter(int[][] grid) {
int n = grid.length, m = grid[0].length;
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 1) {
for (int k = 0; k < 4; k++) {
int ni = i + dx[k];
int nj = j + dy[k];
// 相邻的是边界, 或水域, 贡献+1
if (ni < 0 || ni >= n || nj < 0 || nj >= m || grid[ni][nj] == 0) ans++;
}
}
}
}
return ans;
}
}

额外贴一下DFS的版本,需要注意,DFS过程中,为了避免重复访问某块陆地,我们可能会将已访问过的陆地设为0(变成水域),这会对计算其他陆地对周长的贡献有所影响(因为水域变多了)。可以考虑加一个​​visited​​数组来记录已访问过的陆地。或者将已访问过的陆地置为2。这样,用0表示水域,1表示还未访问过的陆地,2表示已经访问过的陆地。

class Solution {

int[] dx = {1, -1, 0, 0};
int[] dy = {0, 0, 1, -1};
int ans = 0;

public int islandPerimeter(int[][] grid) {
int n = grid.length, m = grid[0].length;
out:
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 1) {
dfs(grid, i, j);
break out; // 因为只有1个岛屿, 提前退出
}
}
}
return ans;
}

private void dfs(int[][] grid, int i, int j) {
int n = grid.length, m = grid[0].length;
grid[i][j] = 2; // visited
for (int k = 0; k < 4; k++) {
int ni = i + dx[k];
int nj = j + dy[k];
if (ni < 0 || ni >= n || nj < 0 || nj >= m || grid[ni][nj] == 0) {
ans++;
continue;
}
if (grid[ni][nj] == 2) continue;
dfs(grid, ni, nj);
}
}
}

当然,这道题并查集也能做,但是没必要,这里就不赘述了。

LeetCode 1905. 统计子岛屿数量

给两个长宽都相等的矩阵grid1和grid2,如果 grid2 的一个岛屿,被 grid1 的一个岛屿 完全 包含,也就是说 grid2 中该岛屿的每一个格子都被 grid1 中同一个岛屿完全包含,那么我们称 grid2 中的这个岛屿为 子岛屿 。

请你返回 grid2 中 子岛屿 的 数目 。

其实就是看grid2中的岛屿。是否被grid1中的完全覆盖。

初步想法是:对grid2进行dfs,找出所有岛屿,并且在dfs遍历的过程中,检查grid1中对应坐标是否也是陆地,即可。

class Solution {
int[] dx = {1, -1, 0, 0};
int[] dy = {0, 0, 1, -1};
public int countSubIslands(int[][] grid1, int[][] grid2) {
int n = grid1.length, m = grid1[0].length;
int num = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid2[i][j] == 1) {
if (dfs(grid1, grid2, i, j)) num++;
}
}
}
return num;
}

// 不用对grid1进行dfs
// grid2需要深搜完该岛的全部节点
private boolean dfs(int[][] grid1, int [][] grid2, int i, int j) {
int n = grid1.length, m = grid1[0].length;
boolean res = grid1[i][j] == 0 ? false : true; // 这个点在grid1中是否存在
grid2[i][j] = 2; // grid2 中标记这个点为已访问过
for (int k = 0; k < 4; k++) {
int ni = i + dx[k];
int nj = j + dy[k];
if (ni < 0 || ni >= n || nj < 0 || nj >= m || grid2[ni][nj] != 1) continue;
res = dfs(grid1, grid2, ni, nj) && res;
}
return res;
}
}

有个地方需要注意,就是这一行

res = dfs(grid1, grid2, ni, nj) && res;

我一开始写成了

res = res && dfs(grid1, grid2, ni, nj);

这样是错误的。因为无论当前这个位置在grid1中是否为陆地,我们都需要将grid2中的这个岛屿搜索完毕(如果不搜索完毕的话,剩余部分在后续的循环中会被当做一个新的岛屿进行搜索)。而当grid1中这个位置不是陆地的话,res会是​​false​​​,而由于​​&&​​ 运算符的短路原则,把res写在前面,会导致后面的dfs操作无法被执行。

这里要特别注意,我debug了半天才发现。

另一种思路:图覆盖。如果grid2中的某个岛,是grid1的子岛,那么这个岛的每块陆地,都会在grid2中出现一次,在grid1中出现一次。

那么,我们遍历grid2,对于grid2中的每一块陆地,找到grid1中对应位置的格子,累加到grid2上。这样,如果grid2的某个岛是子岛,那么这个岛的每块陆地都应该是2。累加完成后,我们只需要对grid2做一次dfs,当一个岛的全部陆地都是2,说明这个岛是一个子岛。

这里可以扩展想一下:不是把grid1全部加到grid2上。全部加过来的话,无法利用岛上的陆地全是2,这个条件来判断子岛。只是对grid2中为1的位置,从grid1中加过来。

也不是把grid2加到grid1,道理同上,无法判断子岛。

class Solution {
int[] dx = {1, -1, 0, 0};
int[] dy = {0, 0, 1, -1};
public int countSubIslands(int[][] grid1, int[][] grid2) {
int n = grid1.length, m = grid1[0].length;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid2[i][j] == 1) grid2[i][j] += grid1[i][j];
}
}

int num = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid2[i][j] == 2) {
if (dfs(grid2, i, j)) num++;
}
}
}
return num;
}

// 如果岛屿中全是2, 则返回true
private boolean dfs(int[][] grid, int i, int j) {
int n = grid.length, m = grid[0].length;
boolean res = grid[i][j] == 2 ? true : false;
grid[i][j] = 3; // visited
for (int k = 0; k < 4; k++) {
int ni = i + dx[k];
int nj = j + dy[k];
if (ni < 0 || ni >= n || nj < 0 || nj >= m) continue;
if (grid[ni][nj] == 0 || grid[ni][nj] == 3) continue; // 水域或者已访问过
res = dfs(grid, ni, nj) && res;
}
return res;
}
}

LeetCode 1254. 封闭岛屿数量

封闭岛屿的含义是,岛屿四周全是水。

解法同样是DFS,遍历过程中看是否碰到边界即可。岛屿题已经做太多了。这里直接贴代码了。

class Solution {
int[] dx = {1, -1, 0, 0};
int[] dy = {0, 0, 1, -1};
public int closedIsland(int[][] grid) {
int n = grid.length, m = grid[0].length;
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 0 && dfs(grid, i, j)) ans++;
}
}
return ans;
}

private boolean dfs(int[][] grid, int i, int j) {
int n = grid.length, m = grid[0].length;
grid[i][j] = 1; // 置为水域
boolean res = true; // 是否全被水域包裹
for (int k = 0; k < 4; k++) {
int ni = i + dx[k];
int nj = j + dy[k];
if (ni < 0 || ni >= n || nj < 0 || nj >= m) {
res = false; // 触到边界, 没有被水包裹
continue;
}
if (grid[ni][nj] == 1) continue; // 周围是水
res = dfs(grid, ni, nj) && res;
}
return res;
}
}

LeetCode 695. 最大岛屿

这道题求解的是最大的岛屿的面积。同样的,我们还是可以用DFS/BFS/并查集,因为无论对岛屿做什么计算,都需要遍历岛屿的每块陆地,或者将相邻的陆地进行合并。下面考虑用DFS来做,将访问过的陆地,置为2。

class Solution {
int[] dx = {1, -1, 0, 0};
int[] dy = {0, 0, 1, -1};
int ans = 0;
public int maxAreaOfIsland(int[][] grid) {
int n = grid.length, m = grid[0].length;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 1) {
// 计算这块岛屿的面积, 并更新最大值
ans = Math.max(ans, dfs(grid, i, j));
}
}
}
return ans;
}

private int dfs(int[][] grid, int i, int j) {
int n = grid.length, m = grid[0].length;
int res = 1; // 当前这块陆地的面积
grid[i][j] = 2; // visited
for (int k = 0; k < 4; k++) {
int ni = i + dx[k];
int nj = j + dy[k];
if (ni < 0 || ni >= n || nj < 0 || nj >= m) continue;
if (grid[ni][nj] == 0 || grid[ni][nj] == 2) continue;
res += dfs(grid, ni, nj);
}
return res;
}
}

LeetCode 827. 最大人工岛

最多只能把一格0变成1,求问执行此操作后,最大的岛屿面积是多少。

就是填海!将一块水域变成陆地,问能造出的人工岛的最大面积是多少。

最直观的想法当然是,遍历所有的水域,依次看如果将当前水域变成陆地,得到的岛的面积,然后取一个最值。

然而还要考虑一种情况,就是最大的岛是一个自然岛(无需将水域变成陆地),而不是一个人工岛。此种情况只会出现在,所有的格子都是陆地的情况下,此时没有海可以填。(但凡存在水域,就一定能通过将一块水域变成陆地,使得某个最大岛的面积变大)。

那么对于一块水域,如何计算将其变成陆地后,能够形成的岛屿面积呢?将一块水域变成陆地,则其最多能连接上下左右4个方向的岛屿。我们只需要将其4个方向中的陆地所在的岛屿,进行连接即可。

我对这道题的直观感觉是并查集,因为可以通过并查集进行集合合并,并维护连通块的大小(岛屿面积)。

需要注意,若一块水域的2个方向上都是陆地,这2块陆地有可能属于同一个岛,此时不能重复计算其面积。(即,需要去重)

下面贴一个纯并查集的实现

class Solution {
int[] dx = {1, -1, 0, 0};
int[] dy = {0, 0, 1, -1};
int[] p;
int[] cnt; // 额外维护某个连通块的面积
int ans = 1;

public int largestIsland(int[][] grid) {
int n = grid.length, m = grid[0].length;
p = new int[n * m];
cnt = new int[n * m];

for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 1) {
int idx = i * m + j;
p[idx] = idx;
cnt[idx] = 1;
}
}
}

for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 1) {
if (i + 1 < n && grid[i + 1][j] == 1) union(i * m + j, (i + 1) * m + j);
if (j + 1 < m && grid[i][j + 1] == 1) union(i * m + j, i * m + j + 1);
}
}
}

for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 0) {
int temp = 1; // 当前这个位置变成1, 则面积至少为1
Set<Integer> set = new HashSet<>(); // 用于去重
for (int k = 0; k < 4; k++) {
int ni = i + dx[k];
int nj = j + dy[k];
if (ni < 0 || ni >= n || nj < 0 || nj >= m || grid[ni][nj] == 0) continue;
int idx = ni * m + nj;
if (set.contains(find(idx))) continue; // 该岛已经计算过, 不重复计算
temp += cnt[find(idx)];
set.add(find(idx));
}
ans = Math.max(ans, temp);
}
}
}
return ans;
}

private void union(int x, int y) {
int px = find(x), py = find(y);
if (px != py) {
p[px] = py;
cnt[py] += cnt[px];
ans = Math.max(cnt[py], ans); // 在合并过程中, 让ans等于最大的岛的面积
}
}

private int find(int x) {
if (x != p[x]) p[x] = find(p[x]);
return p[x];
}
}

这道题也能用DFS来做,我们可以用DFS来遍历一个岛,并给这个岛进行编号(以便唯一标识一个岛)。并且,我们对岛上所有的陆地块,将其编号置为岛的编号。这样就能方便地进行去重,以及获取岛的面积。

class Solution {
int[] dx = {1, -1, 0, 0};
int[] dy = {0, 0, 1, -1};
int[] area; // 岛屿面积, 下标是岛屿编号
int idx = 2; // 岛屿编号, 从2开始, 因为0是水域, 1是陆地
int ans = 0;
public int largestIsland(int[][] grid) {
int n = grid.length, m = grid[0].length;
area = new int[n * m + 2]; // 对于[1] 这样的输入, 由于岛的编号从2开始, 需要多开2个大小
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 1) {
// 计算这个岛的面积
area[idx] = dfs(grid, i, j);
// 最大岛屿面积
ans = Math.max(ans, area[idx]);
// 岛的编号+1
idx++;
}
}
}

// 对所有的水域, 计算可能形成的最大人工岛面积
Set<Integer> set = new HashSet<>(); // 用于去重, 声明在外面, 避免频繁创建对象
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 0) {
set.clear();
int t = 1;
for (int k = 0; k < 4; k++) {
int ni = i + dx[k];
int nj = j + dy[k];
if (ni < 0 || ni >= n || nj < 0 || nj >= m || grid[ni][nj] == 0) continue;
if (set.contains(grid[ni][nj])) continue; // 用set做去重
t += area[grid[ni][nj]];
set.add(grid[ni][nj]); // 已经出现过, 记得添加到set
}
ans = Math.max(ans, t);
}
}
}

return ans;
}

private int dfs(int[][] grid, int i, int j) {
int n = grid.length, m = grid[0].length;
grid[i][j] = idx; // 将这块陆地编号变为岛屿编号
int res = 1; // 面积
for (int k = 0; k < 4; k++) {
int ni = i + dx[k];
int nj = j + dy[k];
if (ni < 0 || ni >= n || nj < 0 || nj >= m) continue; // 边界
if (grid[ni][nj] == 0 || grid[ni][nj] > 1) continue; // 水域或者其他
res += dfs(grid, ni, nj);
}
return res;
}
}

注意2个点:

  • 可能所有岛中,面积最大者,是答案,而不是把某个0变成1后形成的人工岛(这种情况只出现在全部格子都是陆地的时候)
  • 对某个水域,在其上下左右四个方向做连接时,记得去重(可能2个方向上的陆地属于同一个岛,不能重复计算)

小结

岛屿类问题都可以用DFS来解决,需要注意:

  • DFS过程中要标记已访问过的点,防止重复访问(标记方式有很多种,比如访问过的格子置0,置2,开一个​​visited​​ 数组等)
  • DFS的过程可以计算出岛屿的一些属性(周长,面积等)
  • 可以对岛屿进行编号,以便做唯一标识

当然也可以用并查集,因为将相邻的陆地进行连接,形成岛屿的过程,也可以看作是集合合并。

待办

其他待办的follow up:形状相同的岛有多少个?形状不同的岛有多少个?(猜测对应305. 694. 711.这三道题,但是没钱开会员,就先这样吧 TODO)

LeetCode 694. 不同岛屿数量

在网上搜到了这道题的描述:若两个岛屿经过平移后,能够完全重合,则称这两个岛屿形状相同。

有一个思路很巧妙:如何判断2个岛屿具有相同形状?根据遍历这个岛屿每块陆地的顺序

只要两个岛屿,其遍历的顺序是一致的,说明其形状相同。太妙了!下面给出代码,本地调试了一些test cast,结果正确。但没有会员,无法提交到LeetCode验证是否正确。

class Solution {
int[] dx = {1, -1, 0, 0};
int[] dy = {0, 0, 1, -1};
public int numIslands(int[][] grid) {
int n = grid.length, m = grid[0].length;
Set<String> set = new HashSet<>();
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 1) {
// 记录这个岛的形状 (遍历顺序)
StringBuilder sb = new StringBuilder();
dfs(grid, i, j, sb);
set.add(sb.toString());
}
}
}
return set.size();
}

private void dfs(int[][] grid, int i, int j, StringBuilder sb) {
int n = grid.length, m = grid[0].length;
grid[i][j] = 0;
for (int k = 0; k < 4; k++) {
int ni = i + dx[k];
int nj = j + dy[k];
if (ni < 0 || ni >= n || nj < 0 || nj >= m || grid[ni][nj] == 0) continue;
//10 表示向下移动
//-10 表示向上移动
//01 表示向右
//0-1 表示向左
sb.append(dx[k]).append(dy[k]);
dfs(grid, ni, nj, sb);
}
}
}

LeetCode 711. 不同岛屿数量II

694的升级版,694对形状相同的岛屿的定义是:经过平移后能够完全重合。

711对形状相同的岛屿的定义是:经过平移,旋转,或镜像翻转后,能够完全重合。

比如下图,左上角的岛屿和右下角的岛屿,在694的定义中就是2个不同的岛屿,但在711的定义中就是相同的岛屿。

LeetCode 岛屿系列全解析 200. 463. 1905. 1254. 695. 827. 694. 711_leetcode

现在回过头来思考一下,694中,我们是通过对这个小岛的遍历顺序,来判断形状是否一致的。而711可以对小岛进行旋转,翻转等各种变换。遍历顺序就不管用了。

换一种思路,先针对694,我们除了小岛的遍历顺序,还有其他方式来描述一个小岛的形状吗?有的。

假设对于一个小岛,我们用一个数组​​shape​​保存其所有陆地块的坐标(绝对坐标)。然后对每个陆地块的坐标,减去最左上方陆地块的坐标。这样就变成了相对坐标。若2个小岛形状相同,则其​​shape​​​数组中存储的相对坐标就是相同的。我们可以直接将​​shape​​数组按顺序展开,比较如果相同,说明形状相同。

再来看711,这道题对相同形状的定义就比较蛋疼了。可以进行旋转,镜像翻转等操作。我们列举一下。对于一个点​​[x,y]​​,能进行的变换无非就8种。第一个位置可以是正负x,y,共4种,对应的第二个位置就有2种选择(只有正负,由于第一个位置字母确定了,第二个位置的字母也就确定了)。共8种(其实题目允许的变换就5种,旋转90°,旋转180°,旋转270°,水平翻转,垂直翻转)。而由于我们是用左上角第一个点作为基准,来计算的相对坐标,所以负数什么的不会有影响。只要形状相同,则相对坐标就是相同的。

那么我们对某一个岛,只需要把​​shape​​​数组中的每一个坐标,都进行一下翻转,然后把翻转后的​​shape​​​,重新与左上角坐标进行对齐。这样就得到了一个转换后的形状,由于存储的是相对坐标,那么只要形状相同,​​shape​​数组中存储的所有坐标就是相同的。

这样,对于一个岛,我们存储其变换后可能达到的形状(存储8个​​shape​​数组,每个​​shape​​数组中存的是一系列点的坐标(相对坐标))。用这全部的形状,来标识这个岛的形状。

我们可以将所有形状中的点,进行扁平化,也就是将​​List<List<Pair>>​​扁平化为​​List<Pair>​​,然后对所有点进行一个排序, 再将得到的​​List​​转换成一个字符串(序列化)。这个字符串就能够唯一标识某一个形状。

代码如下,列举了几个 test case,结果是正确的。然而没充钱,没法提交到LeetCode去看实际的结果。等充钱了再来更新吧。

class Solution {
int[] dx = {1, -1, 0, 0};
int[] dy = {0, 0, 1, -1};
public int numIslands(int[][] grid) {
int n = grid.length, m = grid[0].length;
Set<String> set = new HashSet<>(); // 存所有形状, 每一种形状用一个字符串表示
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 1) {
List<Pair> shape = new ArrayList<>(); // 这个岛屿的全部陆地的坐标
dfs(grid, i, j, shape);
String shapeStr = normalize(shape); // 对这个岛屿的形状进行归一化
set.add(shapeStr);
}
}
}
return set.size();
}

private void dfs(int[][] grid, int i, int j, List<Pair> shape) {
int n = grid.length, m = grid[0].length;
shape.add(new Pair(i, j));
grid[i][j] = 2; // visited
for (int k = 0; k < 4; k++) {
int ni = i + dx[k];
int nj = j + dy[k];
if (ni < 0 || ni >= n || nj < 0 || nj >= m || grid[ni][nj] != 1) continue;
dfs(grid, ni, nj, shape);
}
}

// 归一化
private String normalize(List<Pair> shape) {
// 将8种转换全部加入, 然后进行序列化
List<Pair>[] allShapes = new List[8];
allShapes[0] = shape;
for (int i = 1; i < 8; i++) allShapes[i] = new ArrayList<>();
for (Pair p : shape) {
int x = p.i, y = p.j;
allShapes[1].add(new Pair(-x, y));
allShapes[2].add(new Pair(-x, -y));
allShapes[3].add(new Pair(x, -y));
allShapes[4].add(new Pair(y, x));
allShapes[5].add(new Pair(y, -x));
allShapes[6].add(new Pair(-y, x));
allShapes[7].add(new Pair(-y, -x));
}
// 对每个shape进行排序, 确保每个shape中, 左上角的坐标排在第一个位置
for (List<Pair> s : allShapes) Collections.sort(s);
// 对每个shape, 以左上角为基准, 进行坐标修正, 计算相对坐标
for (List<Pair> s : allShapes) {
// 这里可以直接采用倒序遍历, 确保第一个位置的坐标最后才被更新
Pair beginPos = s.get(0);
int beginI = beginPos.i, beginJ = beginPos.j;
for (Pair p : s) {
p.i = p.i - beginI;
p.j = p.j - beginJ;
}
}

// 将每个shape的点全部拿出来, 放在一起, 进行扁平化
List<Pair> allPair = new ArrayList<>();
for (List<Pair> s : allShapes) {
for (Pair p : s) allPair.add(p);
}

// 对扁平化后的点, 进行排序
Collections.sort(allPair);
StringBuilder sb = new StringBuilder();
// 序列化成字符串
for (Pair p : allPair) sb.append(p.toString());
return sb.toString();
}

class Pair implements Comparable<Pair> {

private int i;

private int j;

public Pair(int i, int j) {
this.i = i;
this.j = j;
}

@Override
public int compareTo(Pair o) {
// i越小, 越靠前
// j越大, 越靠前
// 结果就是左上角地点是第一个点 (最小的点)
return this.i != o.i ? this.i - o.i : o.j - this.j;
}

@Override
public String toString() {
// 序列化一个坐标
return "[" + i + "," + j + "]";
}
}
}

注意,必须采用相对坐标,以岛屿左上角第一个点为​​[0,0]​​,这样才能保证相同形状的岛,其坐标从数值上来看是一致的。

在进行归一化时,对每个形状的坐标数组进行排序,是为了确保数组第一个元素是左上角的坐标,以方便计算相对坐标。随后会将全部形状的坐标进行扁平化,放在一个数组中,最后还需要对这个扁平化后的数组进行排序。