算法-图论-深搜/广搜
写在前面的小结
本文主要记录了使用bfs
, dfs
解决图上的问题。
这几题中图的类型主要有两类:
- 岛屿类问题:需要设置directions,对上下左右进行遍历。
- 普通图,用 邻接表、邻接矩阵 记录节点之间的联系。
几个注意点:
- 深搜、广搜 通常需要使用
visited
矩阵,防止对节点的重复遍历。 - 不同的问题需要在遍历过程中记录不同的数值,例如:面积、周长等
1. 岛屿数量 (卡码网 99)
给定一个由 1(陆地)和 0(水)组成的矩阵,你需要计算岛屿的数量。
岛屿由水平方向或垂直方向上相邻的陆地连接而成,并且四周都是水域。你可以假设矩阵外均被水包围。
from collections import deque
# 上下左右
directions = [[0,1], [0,-1], [-1,0], [1,0]]
# 深搜
def dfs(graph, visited, x, y):
visited[x][y] = True
for i in range(4):
next_x = x + directions[i][0]
next_y = y + directions[i][1]
if next_x < 0 or next_x >= len(graph) or next_y < 0 or next_y >= len(graph[0]):
continue
if visited[next_x][next_y] == True or graph[next_x][next_y] == 0:
continue
dfs(graph, visited, next_x, next_y)
# 广搜
def bfs(graph, visited, x, y):
que = deque([])
que.append([x, y])
visited[x][y] = True
while que:
cur_x, cur_y = que.popleft()
for i in range(4):
next_x = cur_x + directions[i][0]
next_y = cur_y + directions[i][1]
if next_x < 0 or next_x >= len(graph) or next_y < 0 or next_y >= len(graph[0]):
continue
if visited[next_x][next_y] == True or graph[next_x][next_y] == 0:
continue
visited[next_x][next_y] = True
que.append([next_x, next_y])
def main():
row_num, col_num = map(int, input().split())
graph = []
visited = [[False for _ in range(col_num)] for _ in range(row_num)]
for i in range(row_num):
graph.append(list(map(int, input().split())))
result = 0
for i in range(row_num):
for j in range(col_num):
if graph[i][j] == 1 and visited[i][j] == False:
result += 1
bfs(graph, visited, i, j)
print(result)
if __name__ == "__main__":
main()
2. 孤岛的面积(卡码网 101)
给定一个由 1(陆地)和 0(水)组成的矩阵,岛屿指的是由水平或垂直方向上相邻的陆地单元格组成的区域,且完全被水域单元格包围。
孤岛是那些位于矩阵内部、所有单元格都不接触边缘的岛屿。
现在你需要计算所有孤岛的总面积,岛屿面积的计算方式为组成岛屿的陆地的总数。
思路:
- 先将位于边界上的岛屿,访问一遍
visited[x][y] = True
- 再将
result
清零,统计所有孤岛的面积。
# 广搜做法
from collections import deque
directions = [[0,1], [0,-1], [-1,0], [1,0]]
result = 0
def bfs(graph, visited, x, y):
# 声明 result 是一个全局变量
global result
deq = deque([])
visited[x][y] = True
result += 1
deq.append([x, y])
while deq:
cur_x, cur_y = deq.popleft()
for i in range(4):
next_x = cur_x + directions[i][0]
next_y = cur_y + directions[i][1]
if next_x < 0 or next_x >= len(graph) or next_y < 0 or next_y >= len(graph[0]):
continue
if graph[next_x][next_y] == 0 or visited[next_x][next_y]:
continue
visited[next_x][next_y] = True
result += 1
deq.append([next_x, next_y])
def main():
num_row, num_col = map(int, input().split())
graph = []
visited = [[False for _ in range(num_col)] for _ in range(num_row)]
for _ in range(num_row):
graph.append(list(map(int, input().split())))
# 沿着四条边界,将处在边界上的岛屿的visited都标记为True
for j in range(num_col):
if graph[0][j] == 1 and visited[0][j] == False:
bfs(graph, visited, 0, j)
if graph[num_row-1][j] == 1 and visited[num_row-1][j] == False:
bfs(graph, visited, num_row-1, j)
for i in range(num_row):
if graph[i][0] == 1 and visited[i][0] == False:
bfs(graph, visited, i, 0)
if graph[i][num_col-1] == 1 and visited[i][num_col-1] == False:
bfs(graph, visited, i, num_col-1)
# 这些边界上的岛屿的面积不计入结果
global result
result = 0
# 对于内部的所有岛屿计算总面积
for i in range(1, num_row):
for j in range(1, num_col):
if graph[i][j] == 1 and visited[i][j] == False:
bfs(graph, visited, i, j)
print(result)
if __name__ == "__main__":
main()
3. 太平洋大西洋水流问题(LeetCode 417)
现有一个 N × M 的矩阵,每个单元格包含一个数值,这个数值代表该位置的相对高度。
矩阵的左边界和上边界被认为是第一组边界,而矩阵的右边界和下边界被视为第二组边界。
矩阵模拟了一个地形,当雨水落在上面时,水会根据地形的倾斜向低处流动,但只能从较高或等高的地点流向较低或等高并且相邻(上下左右方向)的地点。
我们的目标是确定那些单元格,从这些单元格出发的水可以达到第一组边界和第二组边界。
思路:
- 暴力做法:从每个点开始进行一次dfs,时间复杂度为 O(n^4)
- 优化后每个节点最多访问两次,时间复杂度为O(2*n^2)
class Solution:
def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]:
# flag=1表示从边界1出发进行访问,flag=2表示从边界2出发进行访问
def dfs(heights, visited, x, y, flag):
directions = [[0,1], [0,-1], [-1, 0], [1, 0]]
if visited[x][y] == 0:
visited[x][y] = flag
elif visited[x][y] == 1 and flag == 2:
visited[x][y] = 3
for i in range(4):
next_x = x + directions[i][0]
next_y = y + directions[i][1]
if next_x < 0 or next_x >= len(heights) or next_y < 0 or next_y >= len(heights[0]):
continue
if flag == visited[next_x][next_y] or visited[next_x][next_y] == 3:
continue
# 从边界往中间逆流而上
if heights[next_x][next_y] >= heights[x][y]:
dfs(heights, visited, next_x, next_y, flag)
num_row = len(heights)
num_col = len(heights[0])
# 0表示无法从任一边界到达(i,j),
# 1表示只能从边界1到达,
# 2表示只能从边界2到达
# 3表示从2种边界都能到达
visited = [[0 for _ in range(num_col)] for _ in range(num_row)]
# 从边界1反向dfs
# 上边界
for j in range(num_col):
dfs(heights, visited, 0, j, 1)
# 左边界
for i in range(num_row):
dfs(heights, visited, i, 0, 1)
# 从边界2反向dfs
# 下边界
for j in range(num_col):
dfs(heights, visited, num_row-1, j, 2)
# 右边界
for i in range(num_row):
dfs(heights, visited, i, num_col-1, 2)
result = []
for i in range(num_row):
for j in range(num_col):
if visited[i][j] == 3:
result.append([i, j])
return result
4. 最大人工岛(LeetCode 827)
给你一个大小为 n x n 二进制矩阵 grid 。最多只能将一格 0 变成 1 。
返回执行此操作后,grid 中最大的岛屿面积是多少?
岛屿 由一组上、下、左、右四个方向相连的 1 形成。
class Solution:
def largestIsland(self, grid: List[List[int]]) -> int:
# 上下左右
directions = [[0,1], [0,-1], [-1,0], [1,0]]
global area # 记录每个单个岛屿的面积
def dfs(grid, visited, x, y, islandId):
global area
visited[x][y] = True
grid[x][y] = islandId
area += 1
for i in range(4):
next_x = x + directions[i][0]
next_y = y + directions[i][1]
if next_x >= 0 and next_x < len(grid) and next_y >= 0 and next_y < len(grid[0]):
if visited[next_x][next_y] == False and grid[next_x][next_y] > 0:
dfs(grid, visited, next_x, next_y, islandId)
result = 0
islandId = 2 # 岛屿的编号从2开始
area = 0
allLand = True # 判断是否全部为陆地
map = dict() # 用于建立岛号与岛屿面积的映射
map[0] = 0
visited = [[False for _ in range(len(grid[0]))] for _ in range(len(grid))]
# 建立每个岛屿和岛屿面积的映射
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j] == 0:
allLand = False
if grid[i][j] == 1 and visited[i][j] == False:
area = 0
dfs(grid, visited, i, j, islandId)
map[islandId] = area
islandId += 1
if allLand:
result = len(grid) * len(grid[0])
return result
islandSet = set()
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j] == 0:
# 用于记录当前0上下左右的岛屿号
islandSet.clear()
curArea = 1
for k in range(4):
next_i = i + directions[k][0]
next_j = j + directions[k][1]
if next_i < 0 or next_i >= len(grid) or next_j < 0 or next_j >= len(grid[0]):
continue
if grid[next_i][next_j] not in islandSet:
islandSet.add(grid[next_i][next_j])
curArea += map[grid[next_i][next_j]]
result = max(result, curArea)
return result
5. 单词接龙(LeetCode 108)
在字典(单词列表) wordList 中,从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列:
- 序列中第一个单词是 beginWord 。
- 序列中最后一个单词是 endWord 。
- 每次转换只能改变一个字母。
- 转换过程中的中间单词必须是字典 wordList 中的单词。
给定两个长度相同但内容不同的单词 beginWord 和 endWord 和一个字典 wordList ,
找到从beginWord
到endWord
的最短转换序列
中的单词数目
。如果不存在这样的转换序列,返回 0。
输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
输出:5
解释:一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。
思路:
- 本题等长单词,差距为1,即为两点相连。
- 这里的做法没有显式地构造图,但本质上是邻接矩阵
- 使用
广度遍历
得到的一定是最短路径
。
class Solution:
# 广度遍历,到达endWord的路径即为最短路径
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
if endWord not in wordList:
return 0
# 判断2个等长的单词是否差距为1
def isConnected(word1: str, word2: str) -> bool:
count = 0
for i in range(len(word1)):
if word1[i] != word2[i]:
count += 1
return count == 1
visited = [False for _ in range(len(wordList))]
que = [[beginWord, 1]]
while que :
s, step = que.pop(0)
if isConnected(s, endWord):
return step+1
for i in range(len(wordList)):
if visited[i] == False and isConnected(s, wordList[i]):
que.append([wordList[i], step+1])
visited[i] = True
return 0
6. 有向图的完全可达性(卡码网 105)
给定一个有向图,包含 N 个节点,节点编号分别为 1,2,...,N。
现从 1 号节点开始,如果可以从 1 号节点的边可以到达任何节点,则输出 1,否则输出 -1。
注意:
grid = defaultdict(list)
,建立的是一个键为数值,值为列表的字典
from collections import defaultdict
def main():
num_node, num_edge = map(int, input().split())
# 邻接表
grid = defaultdict(list)
for _ in range(num_edge):
node1, node2 = map(int, input().split())
grid[node1].append(node2)
reachable = []
# dfs
def dfs(grid, startId):
for i in grid[startId]:
if i not in reachable:
reachable.append(i)
dfs(grid, i)
dfs(grid, 1)
for i in range(2, num_node+1):
if i not in reachable:
print(-1)
return
print(1)
if __name__ == "__main__":
main()
7. 岛屿的周长(LeetCode 464)
给定一个 row x col 的二维网格地图 grid ,其中:grid[i][j] = 1 表示陆地, grid[i][j] = 0 表示水域。
网格中的格子 水平和垂直 方向相连(对角线方向不相连)。
整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。
岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。
计算这个岛屿的周长。
class Solution:
def islandPerimeter(self, grid: List[List[int]]) -> int:
global result
result = 0
def dfs(grid, visited, x, y):
global result
directions = [[0,1], [0,-1], [-1,0], [1,0]]
visited[x][y] = True
# 如果四周都没有1时,周长增加4
result += 4
for i in range(4):
next_x = x + directions[i][0]
next_y = y + directions[i][1]
if next_x >=0 and next_x < len(grid) and next_y >= 0 and next_y < len(grid[0]):
if grid[next_x][next_y] == 1:
# 每相邻一个1,从4中减去1
result -= 1
if visited[next_x][next_y] == False:
dfs(grid, visited, next_x, next_y)
visited = [[False for _ in range(len(grid[0]))] for _ in range(len(grid))]
findOne = False
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j] == 1:
findOne = True
dfs(grid, visited, i, j)
break
if findOne:
break
return result