(中等)给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例:
输入:grid = [
["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]
输出:1
经典的矩阵类题目,矩阵又是图(Graph)的典型类型之一。之前的科研工作就是处理这种数据类型,简单的奖基于图匹配算法的无人机视觉导航定位。
根据题意,此处的二位矩阵可以视为在水平、竖直方向上相邻的 ‘1’ 存在边的无向图。求岛屿数量的本质上就是求无向图内连通分量的个数。直觉上,更容易联想到并查集。但考虑到本文作为图算法的入门级教程,这里主要介绍图的深度优先、广度优先算法。
算法很简单,与树的遍历十分相似,可以说是一个套路,可以简单的将树理解为具有单一有向边的图(即若存在结点A->结点B,则必不存在结点B->结点A)。而无向图的特点即存在边连接的结点可以相互到达,这种特质导致图中的结点存在被重复访问的可能。故相较于树的遍历算法,图需要记录访问的手段。
继续讨论本题思路,无论广度还是深度,整体思路是一样的:遍历二维矩阵,搜索值为 ‘1’ 的结点,以此结点开始一轮新的深度/广度,每次深度/广度时记录访问避免重复访问,最后深度/广度的总次数即为岛屿数量。
创建visited数组记录访问,
class Solution {
//三点钟方向顺时针遍历
const int dx[4] = {0, 1, 0, -1};
const int dy[4] = {1, 0, -1, 0};
public:
int numIslands(vector<vector<char>>& grid) {
int m = grid.size();
int n = grid[0].size();
int ans = 0;
//创建数组记录访问
vector<vector<bool>> visited(m, vector<bool>(n, false));
//遍历整个矩阵,记录总深度次数
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(grid[i][j] == '1' && !visited[i][j]){
ans++;
dfs(grid, visited, i, j);
}
}
}
return ans;
}
//图的深度优先算法实现
void dfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int i, int j){
//校验下标合法和确定递归逻辑
if(i >= 0 && j >= 0 && i < grid.size() && j < grid[0].size() && grid[i][j] == '1' && !visited[i][j]){
visited[i][j] = true;
for(int k = 0; k < 4; k++){
int nx = i + dx[k];
int ny = j + dy[k];
dfs(grid, visited, nx, ny);
}
}
}
};
在整个算法流程内,矩阵数据 ‘1’ 只需读取一次,无需维护,故可以在原矩阵上维护访问记录,无需创建visited数据,节省内存开销。
class Solution {
const int dx[4] = {0, 1, 0, -1};
const int dy[4] = {1, 0, -1, 0};
public:
int numIslands(vector<vector<char>>& grid) {
int m = grid.size();
int n = grid[0].size();
int ans = 0;
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(grid[i][j] == '1'){
ans++;
dfs(grid, i, j);
}
}
}
return ans;
}
void dfs(vector<vector<char>>& grid, int i, int j){
if(i >= 0 && j >= 0 && i < grid.size() && j < grid[0].size() && grid[i][j] == '1'){
//将访问过的陆地置为0,配合深度递归逻辑,即可避免重复访问
grid[i][j] = 0;
for(int k = 0; k < 4; k++){
int nx = i + dx[k];
int ny = j + dy[k];
dfs(grid, nx, ny);
}
}
}
};
图的广度优先与树的层序遍历极度相似,几乎是一个模板,都依赖一个队列管理结点的出队入队。
class Solution {
const int dx[4] = {0, 1, 0, -1};
const int dy[4] = {1, 0, -1, 0};
public:
int numIslands(vector<vector<char>>& grid) {
int m = grid.size();
int n = grid[0].size();
int ans = 0;
//创建队列
queue<pair<int, int>> que;
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
//满足条件,开始广度遍历,记录广度次数
if(grid[i][j] == '1'){
ans++;
//入队
que.push({i, j});
//注意置为'0'
grid[i][j] = '0';
while(!que.empty()){
auto note = que.front();
//出队
que.pop();
for(int k = 0; k < 4; k++){
int nx = note.first + dx[k];
int ny = note.second + dy[k];
if(nx >= 0 && ny >= 0 && nx < grid.size() && ny < grid[0].size() && grid[nx][ny] == '1'){
que.push({nx, ny});
//注意置为'0'
grid[nx][ny] = '0';
}
}
}
}
}
}
return ans;
}
};