岛屿问题是一类图的遍历问题,需要在图的结构上进行DFS(深度优先搜索),虽然理论懂但是上手就抓瞎,岛屿问题是网格DFS的代表问题,正好结合讲解学习一下,主要参考这里。
典型的岛屿问题有如下:
岛屿数量
岛屿周长
岛屿的最大面积
网格搜索问题
网格搜索问题就是在由
m
×
n
m\times n
m×n个小方格组成的网格上进行搜索。在岛屿类问题中,认为网格中数字为
0
0
0的为海洋、数字为
1
1
1的是陆地,当相邻的格子数字相同时,就看作就一片连起来的岛屿。
根据图就可以引申出岛屿面积、岛屿周长等一系列问题,而这类问题都可以使用DFS遍历的方法来解答。
网格的DFS结构
首先,DFS是深度优先搜索,应用递归的思想,他的两个关键点在于:
1. 访问子结点
2.判断终止条件
- 访问子结点需要确定子结点有哪些。对于二叉树的一个结点
root
来说,子结点就是它的左子结点root.left
和右子节点root.right
。而对于网格类问题,一个结点root
的子结点是包括上下左右的相邻网格root.left
、root.right
、root.up
、root.down
,因此需要注意递归调用的结点定义不同。 - 判断终止条件。对于一般二叉树而言,递归的终止条件为
root == null
。这时则需要向上返回,防止空指针异常。而对于网格类问题,他的终止条件应当是判断——此结点是否在网格范围内。如图所以,这是一个5x5的网格grid,则若结点grid[r][c]
的r和c的不在5x5的范围内,则说明该结点不存在,需要返回递归。
讲上述的DFS框架转换为代码语言,则有:
//判断是否为可用岛屿
func inAera(grid [][]int,r int,c int) bool{
if r>=0 && r<=len(grid) && c>=0 && c<=len(grid[0]){
return true
}
return false
}
func dfs(grid [][]int,r int,c int) {
//超出网格范围,直接返回
if !inAera(grid,r,c){
return;
}
//访问所有子结点
dfs(grid,r-1,c)
dfs(grid,r+1,c)
dfs(grid,r,c+1)
dfs(grid,r,c-1)
}
避免重复遍历
设置一个标签来确定该结点是否被访问过
//判断是否为可用岛屿
func inAera(grid [][]int,r int,c int) bool{
if r>=0 && r<=len(grid) && c>=0 && c<=len(grid[0]){
return true
}
return false
}
func dfs(grid [][]int,r int,c int) {
//超出网格范围,直接返回
if !inAera(grid,r,c){
return;
}
//判断是否为岛屿网格,不是则回退
if grid[r][c]!=1{
return
}
//将访问过的结点设为2
grid[r][c]=2
//访问所有子结点
dfs(grid,r-1,c)
dfs(grid,r+1,c)
dfs(grid,r,c+1)
dfs(grid,r,c-1)
}
有了基本框架,看一看具体问题。
一、岛屿数量问题
基于已有的dfs框架,结合具体问题分析。从终止条件看:
1、当网格非法时,回退返回;
2、当网格被访问过时,回退返回;
在进行岛屿dfs遍历时,将连片的岛屿置为2,表示这是一个被访问过的岛屿
//判断是否为可用岛屿
func inAera(grid [][]byte,r int,c int) bool{
if r>=0 && r<len(grid) && c>=0 && c<len(grid[0]){
return true
}
return false
}
func dfs(grid [][]byte,r int,c int){
//超出网格范围,直接返回
if !inAera(grid,r,c){
return;
}
//判断是否为岛屿网格,不是则回退
if grid[r][c]!='1'{
return
}
//将访问过的结点设为2
grid[r][c]='2'
//访问所有子结点
dfs(grid,r-1,c)
dfs(grid,r+1,c)
dfs(grid,r,c+1)
dfs(grid,r,c-1)
}
func numIslands(grid [][]byte) int {
if len(grid)==0{
return 0
}
nums:=0
for r:=0;r<len(grid);r++{
for c:=0;c<len(grid[0]);c++ {
if grid[r][c]=='1'{
dfs(grid,r,c)
nums++
}
}
}
return nums
}
二、岛屿周长问题
基于已有的dfs框架,结合具体问题分析。从终止条件看:
1、当网格非法时,回退返回;
2、当网格被访问过时,回退返回;
3、在进行岛屿dfs遍历时,将连片的岛屿置为2,表示这是一个被访问过的岛屿
4、如果一个陆地网格的子结点为水网格,则需要周长增加一
//判断是否为可用岛屿
func inAera(grid [][]int,r int,c int) bool{
if r>=0 && r<len(grid) && c>=0 && c<len(grid[0]){
return true
}
return false
}
func dfs(grid [][]int,r int,c int) int{
//超出网格范围,说明在边界,周长+1
if !inAera(grid,r,c){
return 1;
}
//如果是水网格,说明是岛屿边界,周长+1
if grid[r][c]==0{
return 1
}
//如果网格已被访问过,则回退
if grid[r][c]!=1{
return 0
}
//将访问过的结点设为2
grid[r][c]=2
//访问所有子结点
return dfs(grid,r-1,c) + dfs(grid,r+1,c) +dfs(grid,r,c+1) + dfs(grid,r,c-1)
}
func islandPerimeter(grid [][]int) int {
if len(grid)==0{
return 0
}
ans:=0
for r:=0;r<len(grid);r++{
for c:=0;c<len(grid[0]);c++ {
if grid[r][c]==1{
ans=dfs(grid,r,c)
}
}
}
return ans
}