题目地址:
https://www.lintcode.com/problem/number-of-distinct-islands/description
给定一个 0 − 1 0-1 0−1矩阵,只含 1 1 1的连通块构成一个岛屿,如果两个岛屿可以通过平移重合则视为同一种。问这个矩阵里有多少个不同的岛屿。
思路是DFS,问题的关键在于如何记录岛屿的形状。我们可以在发现 1 1 1的时候,按照固定的顺序对这个岛屿的 1 1 1进行遍历,比如,按照“右下左上”的方式进行遍历,然后用一个StringBuilder记录每一步走的是哪个方向,这样就可以描述出这个岛屿的形状;同时需要注意,回溯的时候这个StringBuilder也需要有所记录,否则就会造成不同岛屿对应同一个字符串的现象。代码如下:
import java.util.HashSet;
import java.util.Set;
public class Solution {
/**
* @param grid: a list of lists of integers
* @return: return an integer, denote the number of distinct islands
*/
public int numberofDistinctIslands(int[][] grid) {
// write your code here
if (grid == null || grid.length == 0 || grid[0].length == 0) {
return 0;
}
// 开个数组存四个方向,DFS的时候就按照这个方向进行遍历
int[][] dirs = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
// set存遍历过的岛屿的形状对应的字符串
Set<String> set = new HashSet<>();
// 记录哪些点已经被访问过
boolean[][] visited = new boolean[grid.length][grid[0].length];
// 按照每一行从左向右,然后从上向下遍历行的顺序对矩阵进行遍历
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
// 发现了未遍历过的岛屿,即进行遍历,同时用一个StringBuilder记录路径的走法
if (!visited[i][j] && grid[i][j] == 1) {
StringBuilder sb = new StringBuilder();
dfs(i, j, grid, sb, visited, dirs);
set.add(sb.toString());
}
}
}
return set.size();
}
// 从grid[x][y]开始遍历一个岛屿,遍历的同时标记这个岛屿的所有1为已访问过;
// 用一个StringBuilder遍历路径的走法,以此确定岛屿的形状
private void dfs(int x, int y, int[][] grid, StringBuilder sb, boolean[][] visited, int[][] dirs) {
visited[x][y] = true;
for (int i = 0; i < dirs.length; i++) {
int nextX = x + dirs[i][0], nextY = y + dirs[i][1];
if (inBound(nextX, nextY, grid) && !visited[nextX][nextY] && grid[nextX][nextY] == 1) {
sb.append(i);
dfs(nextX, nextY, grid, sb, visited, dirs);
// 回溯的时候加个#做一下标记
sb.append('#');
}
}
}
private boolean inBound(int x, int y, int[][] grid) {
return 0 <= x && x < grid.length && 0 <= y && y < grid[0].length;
}
}
时空复杂度 O ( m n ) O(mn) O(mn)。
至于为什么上面存的字符串会和岛屿形状一一对应,可以用数学归纳法证明。首先如果岛屿只含有一个 1 1 1,那么对应的字符串是空串,结论成立;假设岛屿含有 1 1 1的个数小于等于 k k k的时候都成立,当岛屿含 1 1 1的个数是 k + 1 k+1 k+1时,第一个格子被遍历后,从接下来进入的下一个格子开始将所有与这个格子连通的 1 1 1看成一个岛屿,由归纳假设这个岛屿和算法算出的字符串一一对应,直到回溯到第一个格子的时候会新加一个 # \# #,接着再从第一个格子沿着另一个方向遍历,又由归纳假设这个岛屿和算法算出的字符串一一对应;以此类推,每个“分”岛屿会接到字符串后面,并且以 # \# #分隔,所以整个岛屿都是和字符串一一对应。