BFS(广度优先遍历)图算法总结
首先图算法基本都是用BFS解决的,所以学懂了BFS就会图算法了。
例题一:四周扩散型
leetcode200
leetcode130
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:
11110
11010
11000
00000
输出: 1
示例 2:
输入:
11000
11000
00100
00011
输出: 3
解释: 每座岛屿只能由水平和/或竖直方向上相邻的陆地连接而成。
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
if not grid or not grid[0]:
return 0
row = len(grid)
col = len(grid[0])
res = 0
# dfs 标准逻辑代码,判断条件,更改状态,四周扩散,递归
def dfs(i,j):
if 0<=i<row and 0<=j<col and grid[i][j] == '1':
grid[i][j] = '0'
for x,y in [(-1,0),(1,0),(0,1),(0,-1)]:
dfs(i+x,j+y)
#遍历矩阵
for i in range(row):
for j in range(col):
if grid[i][j] == '1':
dfs(i,j)
res +=1
return res
类似题目:
给定一个二维的矩阵,包含 ‘X’ 和 ‘O’(字母 O)。
找到所有被 ‘X’ 围绕的区域,并将这些区域里所有的 ‘O’ 用 ‘X’ 填充。
示例:
X X X X
X O O X
X X O X
X O X X
运行你的函数后,矩阵变为:
X X X X
X X X X
X X X X
X O X X
解释:
被围绕的区间不会存在于边界上,换句话说,任何边界上的 ‘O’ 都不会被填充为 ‘X’。 任何不在边界上,或不与边界上的 ‘O’ 相连的 ‘O’ 最终都会被填充为 ‘X’。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
class Solution:
def solve(self, board: List[List[str]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
if not board or not board[0]:
return 0
row = len(board)
col = len(board[0])
def bfs(i,j):
if 0<=i<row and 0<=j<col and board[i][j] == 'O':
board[i][j] = 'B'
for x,y in [(-1,0),(1,0),(0,1),(0,-1)]:
bfs(i+x,j+y)
for i in range(row):
if board[i][col-1] == 'O':
bfs(i,col-1)
if board[i][0] == 'O':
bfs(i,0)
for i in range(col):
if board[0][i] == 'O':
bfs(0,i)
if board[row-1][i] == 'O':
bfs(row-1,i)
for i in range(row):
for j in range(col):
if board[i][j] == 'O':
board[i][j] = 'X'
if board[i][j] == 'B':
board[i][j] = 'O'
下面是非简化的标准写法:
class Solution:
def solve(self, board: List[List[str]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
if not board or not board[0]:
return
row = len(board)
col = len(board[0])
def bfs(i, j):
from collections import deque
queue = deque()
queue.appendleft((i, j))
while queue:
i, j = queue.pop()
if 0 <= i < row and 0 <= j < col and board[i][j] == "O":
board[i][j] = "B"
for x, y in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
queue.appendleft((i + x, j + y))
for j in range(col):
# 第一行
if board[0][j] == "O":
bfs(0, j)
# 最后一行
if board[row - 1][j] == "O":
bfs(row - 1, j)
for i in range(row):
if board[i][0] == "O":
bfs(i, 0)
if board[i][col - 1] == "O":
bfs(i, col - 1)
for i in range(row):
for j in range(col):
if board[i][j] == "O":
board[i][j] = "X"
if board[i][j] == "B":
board[i][j] = "O"
其实吧bfs都必须有一个队列来负责存储扩散情况的,但是由于上面例子的情况简单只有四周四个方向,所以在for循环列表里面就包括了,就不另外建立新列表了。下面情况复杂所以得把for循环和列表分别处理。
小改进的一道题:
leetcode542 01矩阵
给定一个由 0 和 1 组成的矩阵,找出每个元素到最近的 0 的距离。
两个相邻元素间的距离为 1 。
示例 1:
输入:
0 0 0
0 1 0
0 0 0
输出:
0 0 0
0 1 0
0 0 0
示例 2:
输入:
0 0 0
0 1 0
1 1 1
输出:
0 0 0
0 1 0
1 2 1
这道题不能用上面的那种简单的递归bfs,因为我们需要计算走的步数,所以用递归的话,就涉及到步数累加操作,写起来比较麻烦。
from collections import deque
class Solution:
def updateMatrix(self, matrix: List[List[int]]) -> List[List[int]]:
if not matrix or not matrix[0]:
return []
n = len(matrix)
m = len(matrix[0])
queue = deque()
#创建一个新矩阵用来储存结果,这样方便计算步数
res = [[-1]*m for _ in range(n)]
for i in range(n):
for j in range(m):
#反向思维,求0到1的步数
if matrix[i][j] == 0:
res[i][j] = 0
queue.append([i,j])
while queue:
i,j = queue.popleft()
for x,y in [(-1,0),(1,0),(0,1),(0,-1)]:
new_i,new_j = i+x,j+y
if 0<=new_i<n and 0<=new_j<m and res[new_i][new_j]==-1:
res[new_i][new_j] = res[i][j] +1
queue.append([new_i,new_j])
return res
例题二:
leetcode127
给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:
每次转换只能改变一个字母。
转换过程中的中间单词必须是字典中的单词。
说明:
如果不存在这样的转换序列,返回 0。
所有单词具有相同的长度。
所有单词只由小写字母组成。
字典中不存在重复的单词。
你可以假设 beginWord 和 endWord 是非空的,且二者不相同。
示例 1:
输入:
beginWord = “hit”,
endWord = “cog”,
wordList = [“hot”,“dot”,“dog”,“lot”,“log”,“cog”]
输出: 5
解释: 一个最短转换序列是 “hit” -> “hot” -> “dot” -> “dog” -> “cog”,
返回它的长度 5。
示例 2:
输入:
beginWord = “hit”
endWord = “cog”
wordList = [“hot”,“dot”,“dog”,“lot”,“log”]
输出: 0
解释: endWord “cog” 不在字典中,所以无法进行转换。
from typing import List
from collections import deque
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
word_set = set(wordList)
if len(word_set) == 0 or endWord not in word_set:
return 0
if beginWord in word_set:
word_set.remove(beginWord)
#储存队列
queue = deque()
queue.append(beginWord)
#防止重复队列
visited = set(beginWord)
visited.add(beginWord)
step = 1
while queue:
current_size = len(queue)
#queue中可能有多个与上一次迭代的单词相差一个编辑距离的单词
#不要直接用len
for i in range(current_size):
word = queue.popleft()
word_list = list(word)
for j in range(len(word_list)):
origin_char = word_list[j]
#扩散寻找
for k in range(26):
word_list[j] = chr(ord('a') + k)
next_word = ''.join(word_list)
if next_word in word_set:
#判断是否到达终点
if next_word == endWord:
return step + 1
if next_word not in visited:
queue.append(next_word)
word_set.remove(next_word)
#跟回溯一样返回原状态,因为还要与单词下一位判断
word_list[j] = origin_char
step += 1
return 0
例题三:(拓扑排序)
leetcode210
现在你总共有 n 门课需要选,记为 0 到 n-1。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。
可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
示例 1:
输入: 2, [[1,0]]
输出: [0,1]
解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
示例 2:
输入: 4, [[1,0],[2,0],[3,1],[3,2]]
输出: [0,1,2,3] or [0,2,1,3]
解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
想要看懂这道题,应该先了解一下拓扑排序和图的相关知识下面是简单讲解链接
简单讲解
from collections import deque
class Solution:
def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
#入度表
indegrees = [0 for _ in range(numCourses)]
#指向关系,也叫邻接表
adjacency = [[] for _ in range(numCourses)]
#入度为零的队列
queue = deque()
#结果队列
res = []
# Get the indegree and adjacency of every course.
#遍历关系,建立入度表和邻接表
for cur, pre in prerequisites:
indegrees[cur] += 1
adjacency[pre].append(cur)
# Get all the courses with the indegree of 0.
#获得所有起始入度为0的数
for i in range(len(indegrees)):
if not indegrees[i]:
queue.append(i)
# BFS TopSort.
#求解关系,查看所给是否为一有向无环图
while queue:
pre = queue.popleft()
res.append(pre)
#队列出一个元素则总数减一,
#若numCourses为0则说明最后所有元素入度为0
numCourses -= 1
#入度表相关元素减一
for cur in adjacency[pre]:
indegrees[cur] -= 1
#若入度表中有元素值为0,则进入queue
if not indegrees[cur]:
queue.append(cur)
return res if not numCourses else []
例题四:岛屿的周长
leetcode 463
给定一个包含 0 和 1 的二维网格地图,其中 1 表示陆地 0 表示水域。
网格中的格子水平和垂直方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。
岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。
示例 :
输入:
[[0,1,0,0],
[1,1,1,0],
[0,1,0,0],
[1,1,0,0]]
输出: 16
class Solution:
def islandPerimeter(self, grid: List[List[int]]) -> int:
row = len(grid)
col = len(grid[0])
res = 0
for i in range(row):
for j in range(col):
if grid[i][j] == 1:
for x,y in [(-1,0),(1,0),(0,1),(0,-1)]:
new_row,new_col = i+x,j+y
#因为每次都只在岛屿的位置移动一步,所以不满足条件边数加1即可
if not (0<=new_row<row and 0<=new_col<col) or grid[new_row][new_col]==0:
res +=1
return res