[python刷题模板] 双向BFS
一、 算法&数据结构
1. 描述
第一次接触双向BFS,记录一下
在朴素BFS中,时间和空间的消耗通常取决于状态数
的多少,这个数字随着层深加深,是指数级的(不剪枝的情况)。
- 很多最小步数的题都是用层先BFS,层数天然就是步数,当每层扩展状态时操作数很多时,可以使用双向BFS。
- 双向BFS就是从首尾两端同时对向搜索,记录两个visited,当扩展的新状态出现在对方的visited,说明两个方向交汇。
- 当遇到首尾两端是确定的值,而每次扩展状态的操作数很大(如3000)时,单向BFS的时/空复杂度是指数级增长的cn,这时拆分成双向BFS,可以很可观的加快速度.
比较朴素BFS和双向BFS
朴素BFS
- 朴素BFS首先需要一个队列,用以储存当前层的状态。(当然之后的状态最终也要存进来)
- 需要一个set来储存已经搜索过的状态,避免重复搜索。
- 步数的储存:
- 可以将第二步的set转化为dict,给每个状态记录最小步数,需要一点额外空间即可(O(n))。
- 可以在状态结构体里增加step字段,生成状态时记录,同样是O(n) 空间。
- 可以裸层先,用全局step,每次处理q都遍历完一层,然后step+1.
- 退出条件:
- 队列为空,表示已经搜完所有路径。
- 检测生成的状态是目标。
- 答案:就是记录的当前层step。
双向BFS
- 双向BFS需要两个队列,储存两个对向搜索方向的状态。
- 需要两个set来储存两个方向上搜索过的状态。
- 步数的储存:
- 基本上用dict存最便于写代码,另外两种也可以,麻烦一点。
- 退出条件:
- 任意队列为空,表示有一方搜完路径,依然无法交汇。
- 生成的状态在对方的已访问列表里。
- 答案:当前方向的step1+对向访问列表里这个状态的step2。
- 补充:每次选择两个方向中
更小
的那个队列进行搜索,显然需要搜的状态数会少一些。
2. 复杂度分析
- 最坏复杂度不变,和朴素BFS相同,甚至当无解时,实际上做了两遍BFS,耗费是乘2的, O(c^n)。
- 当有解时,会提升很多,虽然还是O(c^n)。
3. 常见应用
- 状态转化的最小步数搜索。
4. 常用优化
- 方向选择:从少的队列向多的队列扩展。
- 代码书写方式:两个搜索,可以设置一个函数,通过传参的方式确定方向,大部分代码是复用的。选择队列,对调visited的位置即可。
- 步数可以用两个常量储存,层先法模式处理;或者visited用字典,多点空间。
- 优化建图。
二、 模板代码
1. 状态转化最小步数搜索
例题: 2059. 转化数字的最小运算数
这题单向也能做,甚至更快,因为这次的复杂度实际上是O(nc)*取决于n,实际上规模不大,中间状态只能是0-1000,c=3000,因此无法体现指数级增长。
class Solution:
def minimumOperations(self, nums: List[int], start: int, goal: int) -> int:
visited1 = {start:0}
visited2 = {goal:0}
q1 = deque(visited1.keys())
q2 = deque(visited2.keys())
ops = [add,sub,xor]
def bfs(q,v1,v2): # 方向从v1向v2搜,注意队列选择。
for _ in range(len(q)):
x = q.popleft()
step = v1[x]+1
for num in nums:
for op in ops:
nxt = op(x,num)
if nxt in v2:
return step + v2[nxt]
if 0<=nxt<=1000 and nxt not in v1:
v1[nxt] = v1[x] + 1
q.append(nxt)
return -1
while q1 and q2:
ret = bfs(q1,visited1,visited2) if len(q1) < len(q2) else bfs(q2,visited2,visited1)
# print(visited1,visited2)
if ret != -1:
return ret
return -1
2. 优化建图+双向BFS,搜索最小步数
链接: 127. 单词接龙
本题状态需要在wordlist内转化,且每次只能改变一个单词,因此可以优化建图,在关系中插入虚拟节点。
例如:abc指向虚拟节点 b_c,a_b,ab_。
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
abc = set(wordList)
if endWord not in abc:
return 0
n = len(beginWord)
graph = defaultdict(list)
def add_egde(word):
cs = list(word)
for i in range(n):
t = cs[i]
cs[i] = '*'
v_word = ''.join(cs)
graph[v_word].append(word)
graph[word].append(v_word)
abc.add(v_word)
cs[i] = t
for word in wordList:
add_egde(word)
add_egde(beginWord)
v1 = {beginWord:0}
v2 = {endWord:0}
q1 = deque(v1)
q2 = deque(v2)
def bfs(q,v1,v2):
for _ in range(len(q)):
u = q.popleft()
step = v1[u]
for v in graph[u]:
if v in v2:
return (step+1+v2[v])//2+1
if v not in v1:
v1[v] = step+1
q.append(v)
return 0
while q1 and q2:
ret = bfs(q1,v1,v2) if len(q1) < len(q2) else bfs(q2,v2,v1)
# print(v1,v2)
if ret != 0:
return ret
return 0
3. 搜索最短操作数
链接: 752. 打开转盘锁
思路和上题类似。
这题由于每次每个数只能走一个数,8-7,8-9,这样步数为1,因此可以A*,实测没有双向BFS快。
class Solution:
def openLock(self, deadends: List[str], target: str) -> int:
dead = set(deadends)
start = '0000'
if start in dead:
return -1
if start == target:
return 0
v1 = {start:0}
v2 = {target:0}
q1 = deque(v1)
q2 = deque(v2)
def bfs(q,v1,v2):
for _ in range(len(q)):
x = q.popleft()
step = v1[x]+1
cur = list(x)
for i in range(4):
t = cur[i]
for p in [int(t)+1,int(t)-1]:
p %= 10
cur[i] = str(p)
y = ''.join(cur)
if y in v2:
return step + v2[y]
if y not in dead and y not in v1:
q.append(y)
v1[y] = step
cur[i] = t
return -1
while q1 and q2:
ret = bfs(q1,v1,v2) if len(q1)<len(q2) else bfs(q2,v2,v1)
if ret != -1:
return ret
return -1
4. 多重优化双向BFS
链接: 815. 公交路线
直接对站点建图是n^2的不能行。
哈希了一下站:bus,状态转移的时候走哈希。
这里注意:站点和bus,只要用过的都剪枝。巨量优化。
class Solution:
def numBusesToDestination(self, routes: List[List[int]], source: int, target: int) -> int:
zhan = defaultdict(list)
for i in range(len(routes)):
for z in routes[i]:
zhan[z].append(i)
if source == target:
return 0
if target not in zhan:
return -1
v1 = {source:0}
v2 = {target:0}
vb1 = set()
vb2 = set()
q1 = deque(v1)
q2 = deque(v2)
def bfs(q,v1,v2,vb1):
step = v1[q[0]]+1
for _ in range(len(q)):
x = q.popleft()
for bus in zhan[x]:
if bus not in vb1: # 重要
vb1.add(bus)
for nxt in routes[bus]:
if nxt in v2:
return step+v2[nxt]
if nxt not in v1:
v1[nxt] = step
q.append(nxt)
return -1
while q1 and q2:
ret = bfs(q1,v1,v2,vb1) if len(q1) < len(q2) else bfs(q2,v2,v1,vb2)
if ret != -1:
return ret
return -1
三、其他
- 双向BFS是对单向BFS的时空优化,确实不如朴素bfs简单,遇到故意卡的题再用吧。
四、更多例题
- 773. 滑动谜题 没什么区别,状态自己序列化成字符串或者二进制即可。