0 更新记录
给最近刷到的LeetCode上用深度优先搜索(Depth First Search, DFS)解决的岛屿类问题做个总结。
所有的岛屿类问题可以总结为对二维矩阵的遍历,一般需要寻找连通的区域,相邻格点一般为上下左右4个,这种类型的题目使用DFS是一个比较通用的方法。
1 题目列表
2 题解
200.岛屿数量
思路
遍历整张图,遇到陆地格点后,将与这个格点连通的区域(题目中定义的岛屿)标记为已经访问过,这个过程使用DFS完成。
下次再遇到陆地格点时,检查是否已经被标记为访问过,如果是未访问的格点,说明是一块新的陆地,记录到岛屿数量中,然后将这片岛屿标记为已访问。
不断重复这个过程,直到整张图都被遍历完,就得到了连通区域的数量。
实现细节
以下代码中,使用了vector<vector<bool>> visited
数组来标记(i, j)
位置的格点是否被访问过。
主函数中,遍历所有格点,如果遇到未被标记的陆地格点,则使用DFS递归函数将与这一格点相连的陆地标记为已访问,将岛屿数量的计数加一。
在DFS递归函数中,传入的位置(i, j)
越过边界、是海洋格点或者被标记为已访问,就需要返回退出。这三个条件都是必须要判断的,虽然在主函数中进行了“未被标记的陆地格点”的判断,但是DFS函数是递归调用的,并不总是从主函数进入,所以主函数判断的结果并不总是满足。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| class Solution { public: void dfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int i, int j) { int m = grid.size(); int n = grid[0].size(); if(i < 0 || j < 0 || i >= m || j >= n) return; if(grid[i][j] == '0') return; if(visited[i][j]) return; visited[i][j] = true; dfs(grid, visited, i - 1, j); dfs(grid, visited, i + 1, j); dfs(grid, visited, i, j - 1); dfs(grid, visited, i, j + 1); } int numIslands(vector<vector<char>>& grid) { int m = grid.size(); int n = grid[0].size(); int result = 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(visited[i][j] == false && grid[i][j] == '1') { result++; dfs(grid, visited, i, j); } } } return result; } };
|
优化代码
题目并没有限制对传入的参数gird
是否可以被修改,所以可以充分利用这个空间标记是否已经访问过,从而省去创建visited
数组的开销。在需要标记为已访问的位置(i, j)
将grid[i][j]
修改为其他值可以表示其已经被访问。更简单的做法是直接修改为'0'
,将陆地变为海洋,这样还可以简化DFS代码中的递归返回条件的判断。修改优化后的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| class Solution { public: void dfs(vector<vector<char>>& grid, int i, int j) { int m = grid.size(); int n = grid[0].size(); if(i < 0 || j < 0 || i >= m || j >= n) return; if(grid[i][j] == '0') return; grid[i][j] = '0'; dfs(grid, i - 1, j); dfs(grid, i + 1, j); dfs(grid, i, j - 1); dfs(grid, i, j + 1); } int numIslands(vector<vector<char>>& grid) { int m = grid.size(); int n = grid[0].size(); int result = 0; for(int i = 0; i < m; i++) { for(int j = 0; j < n; j++) { if(grid[i][j] == '1') { result++; dfs(grid, i, j); } } } return result; } };
|
463.岛屿的周长
思路
仍然是对二维矩阵的遍历,只是遍历过程中需要记录边长总和。
对于每一个陆地格点来说,先考虑最基本的某一个方向,并且不能重复访问已经访问过的格点。
当从陆地格点进入下一个陆地格点后,前进的方向没有产生新的边长,因为两块陆地之间的边长并不是岛屿的边长,注意再次说明这里只考虑前进方向这一条边的有无,其他三个方向在向对应方向前进时再统计。
如果进入的下一个格点超出了边界,说明当前的格点就是地图最外侧的陆地格点,前进方向的边应当作为岛屿的边来计算。
同理从当前陆地格点前进到了海洋格点,此时跨过的边也是岛屿的一条边,也需要计入周长。
实现细节
与上一题目类似,主函数还是找一个陆地点,使用DFS搜索。实际上这个题目已经说明只有一个岛屿,所以从主函数只会进入一次DFS函数,实际上从任意一个陆地点(i, j)
进入DFS函数都能得到正确的结果。
DFS函数与之前有个明显的不同是有了返回值,含义是返回与(i, j)
位置的陆地点相连的岛屿的边长。
递归结束条件为之前分析的三种岛屿的边界,对于从陆地进入地图边界和从陆地到海洋这两种情况,当前格点向岛屿贡献的边长增加一条;
对于进入了已经标记为已访问的格点,直接退出并且不新增岛屿的边长。
对于进入了未被标记的陆地点,也不产生边长,此时需要将这个陆地点标记为已访问,并在这个陆地点重复上面的向四周前进并判断的过程(递归调用DFS),并将这个陆地点四周的点的边长贡献总和返回。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class Solution { public: int dfs(vector<vector<int>>& grid, vector<vector<bool>>& visited, int i, int j) { int m = grid.size(); int n = grid[0].size(); if(i < 0 || j < 0 || i >= m || j >= n) return 1; if(visited[i][j]) return 0; if(grid[i][j] == 0) return 1; visited[i][j] = true; return (dfs(grid, visited, i - 1, j) + dfs(grid, visited, i + 1, j) + dfs(grid, visited, i, j - 1) + dfs(grid, visited, i, j + 1)); } int islandPerimeter(vector<vector<int>>& grid) { int m = grid.size(); int n = grid[0].size(); 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) return dfs(grid, visited, i, j); } } return 0; } };
|
优化代码
与200.岛屿数量的优化思路类似,可以对grid
修改避免使用visited
数组,在后续的题目中,如果可以“复用”grid
数组,则都不会再使用visited
数组。
以下是优化后的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class Solution { public: int dfs(vector<vector<int>>& grid, int i, int j) { int m = grid.size(); int n = grid[0].size(); if(i < 0 || j < 0 || i >= m || j >= n) return 1; if(grid[i][j] == 2) return 0; if(grid[i][j] == 0) return 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)); } int islandPerimeter(vector<vector<int>>& grid) { int m = grid.size(); int n = grid[0].size(); 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) return dfs(grid, i, j); } } return 0; } };
|
模拟方法
这个题目还有更为直观的模拟方法,其实个人觉得模拟方法解决这道题更简单易懂,使用DFS有点“没苦硬吃”的感觉。
模拟的思路就是对于一个孤立的陆地格点,它应当拥有四条边。当这个格点上侧有陆地时,就会少一条边;同理其他三个方向都是这种情况。getBorder
函数用于计算传入位置格点能贡献给周长的边数。注意在判断四个相邻点时要先检查索引是否越界。位于边界的边不需要减少,所以边界点和内部点的判断逻辑相同。
主函数中累加每个陆地格点能贡献给岛屿周长的边的数量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class Solution { public: int getBorder(vector<vector<int>>& grid, int i, int j) { int result = 4; if(i > 0 && grid[i - 1][j] == 1) result--; if(i < grid.size() - 1 && grid[i + 1][j] == 1) result--; if(j > 0 && grid[i][j - 1] == 1) result--; if(j < grid[0].size() - 1 && grid[i][j + 1] == 1) result--; return result; } int islandPerimeter(vector<vector<int>>& grid) { int m = grid.size(); int n = grid[0].size(); int result = 0; for(int i = 0; i < m; i++) { for(int j = 0; j < n; j++) { if(grid[i][j] == 1) result += getBorder(grid, i, j); } } return result; } };
|
695.岛屿的最大面积
思路
需要在DFS搜索陆地点构成的岛屿时计算岛屿的面积,参考463.岛屿的周长中使用DFS遍历同时计算周长的思路,可以得到如下结果:
- 从陆地点越过边界:不新增面积
- 从陆地点进入海洋:不新增面积
- 从陆地点进入已访问的陆地:不新增面积
- 从陆地点进入未访问的陆地:新增当前点的面积1,并从当前点继续向四周前进
实现细节
主函数需要维护一个最大面积,遍历每个未被访问的陆地点,然后使用DFS函数计算与这个陆地点相连的岛屿的面积,并将这个岛屿所有点标记为已访问。
DFS递归结束条件为上面分析的三种情况,当前点(i, j)
为边界之外、海洋或已访问过的点时,都不新增面积,直接返回0。
- 每次进入DFS函数时,一定是从陆地点进入的,也就是说当前点
(i, j)
的上一步一定也是陆地点,如果不是,那么在上一步不是陆地点的时候就会退出递归过程。所以在之前分析各种情况时都是“从陆地点进入……”
- 这里使用了修改
grid
来标记已访问,所以grid[i][j] == 0
既可能是海洋也可能是已访问的陆地。
当前点为未访问的陆地,标记访问后,向四周搜索并向上一级返回四周点能够能贡献的面积,加上自己能提供的面积1。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class Solution { public: int dfs(vector<vector<int>>& grid, int i, int j) { int m = grid.size(); int n = grid[0].size(); if(i < 0 || j < 0 || i >= m || j >= n) return 0; if(grid[i][j] == 0) return 0; grid[i][j] = 0; return (1 + dfs(grid, i - 1, j) + dfs(grid, i + 1, j) + dfs(grid, i, j - 1) + dfs(grid, i, j + 1)); } int maxAreaOfIsland(vector<vector<int>>& grid) { int m = grid.size(); int n = grid[0].size(); int result = 0; for(int i = 0; i < m; i++) { for(int j = 0; j < n; j++) { if(grid[i][j] == 1) { result = max(result, dfs(grid, i, j)); } } } return result; } };
|
1254.统计封闭岛屿的数目
思路
直接去判断岛屿封闭的情况想起来可能比较复杂,这时候可以考虑“正难则反”的思路。
- 封闭岛屿的数量 = 总岛屿数量 - 不封闭岛屿的数量
岛屿的总数是容易计算的,在200.岛屿数量中已经计算过。不封闭岛屿是什么呢,就是和边界直接相连的岛屿。
明确了这个情况之后就可以将思路转化为,先排除掉不封闭的岛屿(将其标记为已经访问),然后再遍历全图计算得到的所有岛屿数量就是封闭岛屿数量。
实现细节
主函数中先将与四条边界相连的岛屿用DFS函数标记为已访问的岛屿,然后对全图的未访问的陆地点使用DFS搜索,此处就完全转换为200.岛屿数量中的问题。
DFS实现和200.岛屿数量的优化代码完全一致,只是题目对陆地和海洋的定义相反,本题中陆地用0表示,海洋用1表示。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| class Solution { public: void dfs(vector<vector<int>>& grid, int i, int j) { int m = grid.size(); int n = grid[0].size(); if(i < 0 || j < 0 || i >= m || j >= n) return; if(grid[i][j] == 1) return; grid[i][j] = 1; dfs(grid, i - 1, j); dfs(grid, i + 1, j); dfs(grid, i, j - 1); dfs(grid, i, j + 1); } int closedIsland(vector<vector<int>>& grid) { int m = grid.size(); int n = grid[0].size(); int result = 0; for(int i = 0; i < m; i++) { dfs(grid, i, 0); dfs(grid, i, n - 1); } for(int j = 0; j < n; j++) { dfs(grid, 0, j); dfs(grid, m - 1, j); } for(int i = 0; i < m; i++) { for(int j = 0; j < n; j++) { if(grid[i][j] == 0) { result++; dfs(grid, i, j); } } } return result; } };
|
1905.统计子岛屿
思路
本题部分延续了上一题1254.统计封闭岛屿的数目中“正难则反”的思路,直接去考虑两张图的包含关系可能不是很好判断,那么可以考虑:
先把不可能是grid1
子岛屿的岛屿部分从grid2
中剔除,也就是标记为已访问,然后问题再次转化为计算grid2
中岛屿的数量。
如何判断grid2
中哪些岛屿一定不是grid1
的子岛屿呢?子岛屿要求满足完全包含,所以只要有一个格点不包含,那就一定不是子岛屿。
也就是说,如果grid2
中的一个格点是陆地,但同位置的grid1
是海洋,这个位置就一定不属于子岛屿,和这个位置相连的陆地也不可能是子岛屿。
实现细节
主函数中首先对grid2
中是陆地但grid1
中是水的所有格点用DFS标记,标记为已经访问的区域会在后序搜索中被剔除。然后问题转化为计算grid2
中的岛屿数量,与200.岛屿数量完全相同。
DFS函数实现和200.岛屿数量完全一致。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| class Solution { public: void dfs(vector<vector<int>>& grid, int i, int j) { int m = grid.size(); int n = grid[0].size(); if(i < 0 || j < 0 || i >= m || j >= n) return; if(grid[i][j] == 0) return; grid[i][j] = 0; dfs(grid, i - 1, j); dfs(grid, i + 1, j); dfs(grid, i, j - 1); dfs(grid, i, j + 1); } int countSubIslands(vector<vector<int>>& grid1, vector<vector<int>>& grid2) { int m = grid2.size(); int n = grid2[0].size(); for(int i = 0; i < m; i++) { for(int j = 0; j < n; j++) { if(grid2[i][j] == 1 && grid1[i][j] == 0) { dfs(grid2, i, j); } } } int result = 0; for(int i = 0; i < m; i++) { for(int j = 0; j < n; j++) { if(grid2[i][j] == 1) { result++; dfs(grid2, i, j); } } } return result; } };
|
1020.飞地的数量
思路
本题可以认为是1254.统计封闭岛屿的数目和695.岛屿的最大面积的一个缝合。
题目中的飞地描述为“无法在任意次数的移动中离开网格边界的陆地”即1254.统计封闭岛屿的数目中对封闭岛屿的定义,本题要计算这种“陆地单元格的数量”,也就是计算封闭岛屿的面积。
寻找飞地使用封闭岛屿的思路,先把四周的边界处的陆地标记为已访问的区域,然后遍历整张图,使用计算岛屿面积的算法得到飞地的单元格数量。
实现细节
主函数与1254.统计封闭岛屿的数目的主函数相同;DFS递归函数与695.岛屿的最大面积的DFS函数。
需要注意本题使用0表示一个海洋单元格、1表示一个陆地单元格。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| class Solution { public: int dfs(vector<vector<int>>& grid, int i, int j) { int m = grid.size(); int n = grid[0].size(); if(i < 0 || j < 0 || i >= m || j >= n) return 0; if(grid[i][j] == 0) return 0; grid[i][j] = 0; return (1 + dfs(grid, i - 1, j) + dfs(grid, i + 1, j) + dfs(grid, i, j - 1) + dfs(grid, i, j + 1)); } int numEnclaves(vector<vector<int>>& grid) { int m = grid.size(); int n = grid[0].size(); int result = 0; for(int i = 0; i < m; i++) { dfs(grid, i, 0); dfs(grid, i, n - 1); } for(int j = 0; j < n; j++) { dfs(grid, 0, j); dfs(grid, m - 1, j); } for(int i = 0; i < m; i++) { for(int j = 0; j < n; j++) { if(grid[i][j] == 1) { result += dfs(grid, i, j); } } } return result; } };
|
827.最大人工岛
思路
最为直接的暴力思路是,尝试将每个海洋格点修改为陆地格点,然后计算修改后的岛屿面积,并在计算过程中维护岛屿的最大值。不过这个思路由于每次都要重新计算整张图的岛屿面积,时间复杂度高达 $O \left(n^4 \right)$ ,对于本题n <= 500
的数据量是一定会超时的。时间复杂度高的原因就是,每修改一个点,只影响到了这个点周边的最多四块岛屿的面积,却要对整张图的岛屿面积重复计算。
对于每个海洋格点遍历 $O \left( n \times n \right)$,都计算其修改为陆地后的最大岛屿面积 $O \left(n \times n \right)$,总时间复杂度 $O \left(n^4 \right)$
为了减少岛屿面积的重复计算,可以先计算所有岛屿的面积并保存,然后尝试将每个海洋格点变为陆地,计算这个格点周围的岛屿面积时,使用之前计算的结果,而不再重新DFS搜索计算面积。此时的总时间复杂度是$O \left(n^2 \right)$,是能够满足要求的。
计算所有岛屿面积 $O \left( n^2 \right)$,修改每个海洋格点 $O \left( n^2 \right)$,由于这个算法中这两步是先后进行的,而不是像之前一样嵌套执行,所以总时间复杂度是 $O \left(n^2 + n^2 \right)$,时间复杂度一般不考虑常系数,所以这个思路最终的时间复杂度为$O \left(n^2 \right)$
接下来的关键问题在于如何保存岛屿面积,还需要区分不同的岛屿,区分岛屿可以为不同的岛屿编号。同时保存这两个信息想到使用哈希表,其key
用来存储岛屿的编号,value
存储岛屿的面积。
计算岛屿面积使用695.岛屿的最大面积的方法。DFS搜索时直接将格点的值修改为岛屿编号,修改后的岛屿编号还可以兼具标记已访问的功能,这就需要将岛屿编号修改为非0且非1的值,这里岛屿编号使用2,3,4,……
为什么岛屿编号要是非0非1的值?
编号为0并不影响DFS计算面积,但是在使用哈希表时会出现歧义,key==0
时表示的是0号岛屿的面积还是海洋的面积?这里在编写代码是容易产生困惑;另外在第二次遍历整张图修改岛屿时,如何区分是海洋格点还是0号岛屿呢?这个原因就直接导致了不能使用0作为岛屿编号
编号为1时同理,在DFS搜索时就会遇到问题,对当前点标记为已访问时,如果标记成1(1号岛屿),那下次从其他陆地进入这个格点,如何判断这里是未访问的陆地(原本就是1)还是已经访问过的1号岛屿呢?所以岛屿编号既不能是0,也不能是1
计算得到所有岛屿的面积之后,再次遍历整图,判断每个海洋格点的周围是否有陆地格点,如有就根据陆地格点的编号到哈希表中查询其岛屿面积,将这个点周围的所有岛屿面积加和后再加上这个点本身的面积1,就得到了当前海洋格点变成陆地后的岛屿面积。维护一个最大面积,遍历结束后,得到最终的最大面积。
实现细节
计算面积使用DFS递归函数,修改自695.岛屿的最大面积的DFS函数,标记已访问时将格点内容标记为传入的参数label
,表示岛屿编号,同一片岛屿使用相同的岛屿编号标记。
在判断递归结束条件时,进入海洋格点grid[i][j] == 0
或者已被标记为访问过的格点grid[i][j] >= 2
的判断条件合并为grid[i][j] != 1
。
主函数首先完成岛屿面积的计算,遍历全图未被访问过的陆地格点grid[i][j] == 1
,使用DFS计算面积并标记为编号label
,每标记完一片岛屿后更新label
的值,将计算得到的岛屿面积保存在哈希表unordered_map<int, int> grid_map
中,哈希表的key
为岛屿编号,value
为岛屿面积。
此处需要注意,在计算面积时就要开始维护最终的最大面积结果result
,因为输入中可能存在不需要变海洋为陆地的情况,例如输入样例3中所有格点都是陆地,没有海洋格点可以修改,此时获得的最大面积就是最后输出的最大面积。
接下来需要遍历全图的海洋格点,计算这个格点和周围相连的岛屿的面积,使用area
记录,初始值为当前格点自己的面积1。
此处需要注意,有的海洋格点可能会与同一片岛屿有大于一条临边,如样例2的情况,海洋格点的上侧和左侧都是编号为2的岛屿,这种情况不要重复计算同一片岛屿面积。这里使用一个集合unordered_set<int> used_label
保存已经累加岛屿面积的岛屿编号,在累加面积时先判断当前岛屿是否已经加过了。
代码
由于一直以来不习惯用四次循环+方向数组来对当前格点周围的四个点搜索,在最后检查海洋格点周围四个格点是否有陆地时,写出了vector<int> labels
处极其丑陋的代码,十分影响可读性,但是不想改了。
labels
的作用是经过越界检查后取出当前格点(i, j)
四周的格点的值,用于在循环中判断当前位置四周是否是陆地lb > 1
,如果是陆地就使用lb
去查哈希表得到岛屿面积
如果当前格点是边界点,越界检查时会在labels
中对应越界的位置存入-1
,在循环中判断是否是陆地lb > 1
会直接忽略。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| class Solution { public: int dfs(vector<vector<int>>& grid, int label, int i, int j) { int m = grid.size(); int n = grid[0].size(); if(i < 0 || j < 0 || i >= m || j >= n) return 0; if(grid[i][j] != 1) return 0; grid[i][j] = label; return (1 + dfs(grid, label, i - 1, j) + dfs(grid, label, i + 1, j) + dfs(grid, label, i, j - 1) + dfs(grid, label, i, j + 1)); } int largestIsland(vector<vector<int>>& grid) { int m = grid.size(); int n = grid[0].size(); int result = 0; int label = 2; unordered_map<int, int> grid_map; for(int i = 0; i < m; i++) { for(int j = 0; j < n; j++) { if(grid[i][j] == 1) { grid_map[label] = dfs(grid, label, i, j); result = max(grid_map[label], result); label++; } } } int area = 1; unordered_set<int> used_label; for(int i = 0; i < m; i++) { for(int j = 0; j < n; j++) { if(grid[i][j] != 0) continue; area = 1; used_label.clear(); vector<int> labels{i + 1 < m ? grid[i + 1][j] : -1, i - 1 >= 0 ? grid[i - 1][j] : -1, j + 1 < n ? grid[i][j + 1] : -1, j - 1 >= 0 ? grid[i][j - 1] : -1}; for(int lb : labels) { if(lb > 1 && !used_label.count(lb)) { area += grid_map[lb]; used_label.insert(lb); } } result = max(area, result); } } return result; } };
|