岛屿问题
一句话说明
岛屿题的本质是:在二维网格里找到一个连通块后,用 DFS 或 BFS 一次性把整块陆地“吃干净”,避免重复统计。
先想清楚 DFS 在做什么
- 遇到一块新的陆地,就说明发现了一个新岛屿。
- 从这个格子出发,把上下左右能连到的陆地全部访问掉。
- 整个连通块只会被统计一次。
1 1 0 0
1 0 0 1
0 0 1 1
从左上角开始 DFS:
第一块岛屿全部沉没后,只剩右下角那一大片Go 代码:岛屿数量
func numIslands(grid [][]byte) int {
if len(grid) == 0 || len(grid[0]) == 0 {
return 0
}
var dfs func(i, j int)
dfs = func(i, j int) {
if i < 0 || i >= len(grid) || j < 0 || j >= len(grid[0]) || grid[i][j] == '0' {
return
}
grid[i][j] = '0'
dfs(i+1, j)
dfs(i-1, j)
dfs(i, j+1)
dfs(i, j-1)
}
count := 0
for i := 0; i < len(grid); i++ {
for j := 0; j < len(grid[0]); j++ {
if grid[i][j] == '1' {
dfs(i, j)
count++
}
}
}
return count
}为什么这种写法不会重复计数
关键不变量是:
- 外层循环只在遇到还没访问过的陆地时才
count++。 - 一旦进入 DFS,整块岛屿都会被改写成水域或访问态。
- 所以下次扫描到这块区域时,不会再次进入 DFS。
Go 代码:最大岛屿面积
这题只是把“计数岛屿个数”改成“返回这一整块连通块的大小”。
func maxAreaOfIsland(grid [][]int) int {
if len(grid) == 0 {
return 0
}
var dfs func(i, j int) int
dfs = func(i, j int) int {
if i < 0 || i >= len(grid) || j < 0 || j >= len(grid[0]) || grid[i][j] == 0 {
return 0
}
grid[i][j] = 0
return 1 + dfs(i+1, j) + dfs(i-1, j) + dfs(i, j+1) + dfs(i, j-1)
}
best := 0
for i := 0; i < len(grid); i++ {
for j := 0; j < len(grid[0]); j++ {
if grid[i][j] == 1 {
if area := dfs(i, j); area > best {
best = area
}
}
}
}
return best
}Go 代码:岛屿周长
周长题不一定非要 DFS。更直接的想法是:
- 每块陆地先贡献 4 条边。
- 如果某个方向邻居也是陆地,那这一条边不算外边界。
func islandPerimeter(grid [][]int) int {
if len(grid) == 0 {
return 0
}
perimeter := 0
directions := [][2]int{{0, 1}, {0, -1}, {1, 0}, {-1, 0}}
for i := 0; i < len(grid); i++ {
for j := 0; j < len(grid[0]); j++ {
if grid[i][j] != 1 {
continue
}
for _, d := range directions {
ni, nj := i+d[0], j+d[1]
if ni < 0 || ni >= len(grid) || nj < 0 || nj >= len(grid[0]) || grid[ni][nj] == 0 {
perimeter++
}
}
}
}
return perimeter
}Go 代码:DFS 版周长
如果题目要求你从“连通块递归”的角度理解,也可以这样写:
func islandPerimeterDFS(grid [][]int) int {
var dfs func(i, j int) int
dfs = func(i, j int) int {
if i < 0 || i >= len(grid) || j < 0 || j >= len(grid[0]) || grid[i][j] == 0 {
return 1
}
if grid[i][j] == -1 {
return 0
}
grid[i][j] = -1
return dfs(i+1, j) + dfs(i-1, j) + dfs(i, j+1) + dfs(i, j-1)
}
for i := 0; i < len(grid); i++ {
for j := 0; j < len(grid[0]); j++ {
if grid[i][j] == 1 {
return dfs(i, j)
}
}
}
return 0
}Go 代码:最大人工岛
这一题的关键不是直接暴力翻每个 0,而是先给每个岛屿编号:
- 先把所有原始岛屿染成
2, 3, 4... - 记录每个编号对应的面积
- 再枚举每个
0,看它四周连着哪些不同编号的岛屿
func largestIsland(grid [][]int) int {
n := len(grid)
islandID := 2
areaMap := make(map[int]int)
var dfs func(i, j, id int) int
dfs = func(i, j, id int) int {
if i < 0 || i >= n || j < 0 || j >= n || grid[i][j] != 1 {
return 0
}
grid[i][j] = id
return 1 + dfs(i+1, j, id) + dfs(i-1, j, id) + dfs(i, j+1, id) + dfs(i, j-1, id)
}
for i := 0; i < n; i++ {
for j := 0; j < n; j++ {
if grid[i][j] == 1 {
areaMap[islandID] = dfs(i, j, islandID)
islandID++
}
}
}
best := 0
for _, area := range areaMap {
if area > best {
best = area
}
}
directions := [][2]int{{0, 1}, {0, -1}, {1, 0}, {-1, 0}}
for i := 0; i < n; i++ {
for j := 0; j < n; j++ {
if grid[i][j] != 0 {
continue
}
seen := make(map[int]bool)
area := 1
for _, d := range directions {
ni, nj := i+d[0], j+d[1]
if ni >= 0 && ni < n && nj >= 0 && nj < n {
id := grid[ni][nj]
if id > 1 && !seen[id] {
seen[id] = true
area += areaMap[id]
}
}
}
if area > best {
best = area
}
}
}
if best == 0 {
return 1
}
return best
}四方向遍历写法
dfs(i+1, j)
dfs(i-1, j)
dfs(i, j+1)
dfs(i, j-1)或者:
directions := [][2]int{{0, 1}, {1, 0}, {0, -1}, {-1, 0}}
for _, d := range directions {
dfs(i+d[0], j+d[1])
}易错点
- 岛屿题默认只看上下左右,不看对角线。
- 用修改原数组做访问标记时,别忘了这会破坏输入。
最大人工岛里相邻岛屿编号要去重,否则会重复加面积。- 递归深度很大时,Go 也可能因为深搜层数太深而变慢,这时可以改显式栈。
相关主题
返回:搜索算法