BFS
宽度优先搜索算法(又称广度优先搜索)是最简便的图的搜索算法之一,这一算法也是很多重要的图的算法的原型。Dijkstra单源最短路径算法和Prim最小生成树算法都采用了和宽度优先搜索类似的思想。其别名又叫BFS,属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。
用法
- BFS,其英文全称是Breadth First Search。 BFS并不使用经验法则算法。从算法的观点,所有因为展开节点而得到的子节点都会被加进一个先进先出的队列中。
- 优化:广度搜索的判断重复如果直接判断十分耗时,我们一般借助哈希表来优化时间复杂度。
- DFS的复杂度与BFS的复杂度大体一致,不同之处在于遍历的方式与对于问题的解决出发点不同,DFS适合目标明确,而BFS适合大范围的寻找。
相关题目
127. 单词接龙
题目:给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:
每次转换只能改变一个字母。
转换过程中的中间单词必须是字典中的单词。
- 分析:其实就是将beginWord 单词作为树的根节点,依次向下遍历,看这棵树的叶子结点有没有endWord (有时候也不一定是叶子结点)。如果存在,返回其深度depth,反之返回0。此处有一点需要
- 注意::在树上层出现过的字符串没必要在下层再次出现,因为如果该字符串是转换过程中必须经过的中间字符串,那么应该挑选上层的该字符串继续进行变化,它的转换次数少。
- 常规超时:
- 我们在单词匹配是否只差一个字母的时候用了太多的计算时间, dog, cog,只有中间一位不同,我们可以建立一个字典,其中 *og可以表示后两位位og的三位单词,这样做的话,以来就建立了一个字典,如果右只差一位的单词,就会被聚合到一个字典的键值下面。在进行BFS查找匹配的时候,我们的效率会加快很多
- 还有一种方法就是双向BFS,不止是单向从 begin 到 end, 我们也从 end 查找 begin,这个方法但是我现在还没仔细看,等下次回过头再看。
- 解题模板:
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
# BFS
from collections import deque, defaultdict
# 先验判断
if endWord not in wordList:
return 0
# 提前构建邻接表 -> 用generic state做key
intermidiateWords = defaultdict(list)
wordLen = len(beginWord)
for word in wordList:
for i in range(wordLen):
intermidiateWords[word[:i] + '*' + word[i+1:]].append(word)
# 建队列 加初始状态
queue = deque()
memo = set()
queue.append(beginWord)
memo.add(beginWord)
step = 1
while queue:
size = len(queue)
for _ in range(size):
curWord = queue.popleft()
for i in range(wordLen):
intermidiateCurWord = curWord[:i] + '*' + curWord[i+1:]
# 下一个状态的所有word
for word in intermidiateWords[intermidiateCurWord]:
if word == endWord:
return step + 1
if word not in memo:
queue.append(word)
memo.add(word)
step += 1
else:
return 0
301. 删除无效的括号
题目:删除最小数量的无效括号,使得输入的字符串有效,返回所有可能的结果。说明: 输入可能包含了除 ( 和 ) 以外的字符。
- 分析首先建立一个判断是否括号合法的函数,可以使用计数也可以使用栈。
- 首先加入s到字符串集合中判断当前的集合内的字符串是否有合法的,如果有就提前返回
- 如果没有就要考虑删除字符串的的一些括号,我们每一层的删除之后就传递给下一层,如果下一层还是没有合法的,就继续上一层的删除后的字符串继续删除。
- 总的来说就是每一层对集合内的字符串,每一个字符串选择一个括号删除,所以一个字符串内有多少个括号,一个字符串就有多少个删除可能性,把每一个字符串的删除可能性加入到下一层的集合内部,放到下一层进行判断。
- 解题模板:
class Solution:
def removeInvalidParentheses(self, s: str) -> List[str]:
def isValid(s):#使用栈来判断是否合法
stack = []
for i in range(len(s)):
if s[i] == '(':
stack.append('(')
elif s[i] == ')' :
if stack:
stack.pop()
else:
return False
return True if not stack else False
level = {s}#建立第一个层级,这个层级没有删除元数
# print(level)
while level:#层次BFS
res = []
for s in level:#找这个层级内的合法字串
if isValid(s):
res.append(s)
if res != []: return res
next_level = set()#创建下一个层级
for s in level:
for i in range(len(s)):
if s[i] in ['(',')']:#对当前层级的每一个字符串的每一个括号,进行删除后加入集合
next_level.add(s[:i]+s[i+1:])
level = next_level#集合的赋值,变成当前集合
429. N叉树的层序遍历
题目;给定一个 N 叉树,返回其节点值的层序遍历。 (即从左到右,逐层遍历)。
- 分析:初始化的时候加入根节点。每次进入for循环就是循环当前层的节点个数,进入的时候循环弹出、加入的次数已经固定。所以当这一层循环完,队列里面还剩下的都是下一层的节点。依次加入到输出列表。
- 解题模板:
def levelOrder(self, root: 'Node') -> List[List[int]]:
if root is None:
return []
result = []
queue = collections.deque([root])
while queue:
level = []
for _ in range(len(queue)):
node = queue.popleft()
level.append(node.val)
queue.extend(node.children)
result.append(level)
return result
529. 扫雷游戏
题目:让我们一起来玩扫雷游戏!
给定一个代表游戏板的二维字符矩阵。 ‘M’ 代表一个未挖出的地雷,‘E’ 代表一个未挖出的空方块,‘B’ 代表没有相邻(上,下,左,右,和所有4个对角线)地雷的已挖出的空白方块,数字(‘1’ 到 ‘8’)表示有多少地雷与这块已挖出的方块相邻,‘X’ 则表示一个已挖出的地雷。现在给出在所有未挖出的方块(‘M’或者’E’)的下一个点击位置(行和列索引),根据以下规则,返回相应位置被点击后对应的面板:
如果一个地雷(‘M’)被挖出,游戏就结束了- 把它改为 ‘X’。
如果一个没有相邻地雷的空方块(‘E’)被挖出,修改它为(‘B’),并且所有和其相邻的未挖出方块都应该被递归地揭露。如果一个至少与一个地雷相邻的空方块(‘E’)被挖出,修改它为数字(‘1’到’8’),表示相邻地雷的数量。如果在此次点击中,若无更多方块可被揭露,则返回面板。
- 分析:
- 使用BFS, 首先判断当前的格子是空格还得地雷,如果是地雷就直接修改为X后返回。否则就要对其进行进一步的判断。加入的集合记录表中。
- 每次从队列里面弹出一个坐标,对该坐标周围的8个位置进行遍历,如果周围的八个位置中有地雷,就对地雷计数,最后将该位置设置为地雷的数量。
- 如果该位置的周围八个都不是地雷,或者周围8个有的是空格,有的是已经填上地雷数字的,那么我们就将这些格子加入到队列中,并且将其索引加入到记录集合
- 解题模板:
class Solution:
def updateBoard(self, board: List[List[str]], click: List[int]) -> List[List[str]]:
dirs = [(-1,-1),(-1,0),(-1,1),(0,1),(0,-1),(1,-1),(1,0),(1,1)]
m, n = len(board), len(board[0])
queue = collections.deque()
used = set()
if board[click[0]][click[1]] == 'E':#初始位置的判断
queue.append((click[0], click[1]))#加入队列和记录表
used.add((click[0], click[1]))
else:
board[click[0]][click[1]] = 'X'
return board
while queue:
x0, y0 = queue.popleft()
cnt = 0
temp = set()
for di in dirs:#对每一个方向进行判断
x, y = x0+di[0], y0+di[1]
if 0 <= x < m and 0 <= y < n:
if board[x][y] == 'M':
cnt += 1
elif board[x][y] == 'E' and (x,y) not in used:#对是否使用过要验证一下
temp.add((x,y))
if cnt == 0:#如果该点周围都没有地雷,才可以加入其周围的E
if temp != None:
queue.extend(temp)
used |= temp
board[x0][y0] = 'B'
else:
board[x0][y0] = str(cnt)
return board
752. 打开转盘锁
题目:你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: ‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’ 。每个拨轮可以自由旋转:例如把 ‘9’ 变为 ‘0’,‘0’ 变为 ‘9’ 。每次旋转都只能旋转一个拨轮的一位数字。锁的初始数字为 ‘0000’ ,一个代表四个拨轮的数字的字符串。列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。字符串 target 代表可以解锁的数字,你需要给出最小的旋转次数,如果无论如何不能解锁,返回 -1。
-
分析: 锁盘有四个键,每一个键每一有两种变换的可能,所以个原始输入进去,会产生8个后续的变化可能。但是我们有一个死区dead,和一个走过的数字visited。这两类内的数字就不能在实际产生并且加入到queue队列中。符合条件的数字加入到 queue 和 visited
-
优化:这里我们还考虑是否能使用双向的BFS,因为双向的BFS的使用条件是,我们必须直到起始点和终点。有了这两个点的信息,我们才能够双向的往中间靠,减少复杂度和计算时间。
-
双向BFS:双向BFS和单向BFS在程序上的区别就是,双向的BFS,我们设置的数据类型是list, list也可以左右pop。其次,最重要的是:我们在每次遍历BFS的时候,我们都是遍历的queue, 每次结束遍历的的时候,我们queue2 - > queue,把目标列表赋值给起始列表,其实列表的下一批结果赋值给目标列表。这样做的目的就是,单次交叉BFS,减少中间过程产生的结果。并且每次都只循环一个列表,减少了程序的复杂度。
-
解题模板:
class Solution:
def openLock(self, deadends: List[str], target: str) -> int:
if '0000' in deadends: return -1
queue = []
queue.append('0000')#加入队列
deadends = set(deadends)
queue2 = [target]
visited = set() #建立一个集合,保存走过的路,防止往回走
visited.add('0000')
step = 1
while queue and queue2:
qlen = len(queue)
tempset = []#设置本次queue的下一批可能节点,最后赋值给queue2,交叉BFS
for i in range(qlen):#每次需要循环多少次
cur = queue.pop(0)
for j in range(4):
for k in [-1,1]:
tempcur = cur[:j] + str((int(cur[j])+k+10)%10) + cur[j+1:]
if tempcur not in deadends:
if tempcur in queue2: return step
if tempcur not in visited:
tempset.append(tempcur)#没有走过,加入到队列
visited.add(tempcur)#加入走过的集合
step += 1
queue = queue2#目标值queue2列表赋值给queue
queue2 = tempset#本次queue的下一批节点赋值给queue2
return -1
994. 腐烂的橘子
题目:在给定的网格中,每个单元格可以有以下三个值之一:
值 0 代表空单元格;
值 1 代表新鲜橘子;
值 2 代表腐烂的橘子。
每分钟,任何与腐烂的橘子(在 4 个正方向上)相邻的新鲜橘子都会腐烂。返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1。
- 分析:有以下四种情况分类讨论
- 初始的时候,全部都是新鲜的,返回 -1
- 初始的时候,全部都是坏, 或者没有新鲜的,也没有坏的,返回 0
- 最后BFS之后还有新鲜的,返回-1
- 否则,返回计算的过程数,注意要减一,因为最后一轮坏的需要在BFS弹出一遍
- 解题模板;
class Solution:
def orangesRotting(self, grid: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
queue = collections.deque()
Fresh = 0
for i in range(m):#将腐烂的加入队列,并且看是否还有1
for j in range(n):
if grid[i][j] == 2:
queue.append((i,j))
if grid[i][j] == 1:
Fresh = 1#存在新鲜的
#全是新鲜的 -1, 没有新鲜的也没有坏的0, 只有坏的 0
if not Fresh: return 0
elif Fresh and not queue: return -1
cnt = 0
while queue:
cnt += 1
for _ in range(len(queue)):
i, j = queue.popleft()
for x,y in [(i,j+1), (i,j-1), (i-1,j), (i+1,j)]:
if 0 <= x < m and 0 <= y < n and grid[x][y] == 1:
# print(x,y,cnt)
grid[x][y] = 2
queue.append((x,y))
#最后看下是否还有新鲜的
for i in range(m):
for j in range(n):
if grid[i][j] == 1:return -1
return cnt-1