[python刷题模板] 双向BFS

一、 算法&数据结构

1. 描述

第一次接触双向BFS,记录一下

在朴素BFS中,时间和空间的消耗通常取决于状态数的多少,这个数字随着层深加深,是指数级的(不剪枝的情况)。

  • 很多最小步数的题都是用层先BFS,层数天然就是步数,当每层扩展状态时操作数很多时,可以使用双向BFS。
  • 双向BFS就是从首尾两端同时对向搜索,记录两个visited,当扩展的新状态出现在对方的visited,说明两个方向交汇。
  • 当遇到首尾两端是确定的值,而每次扩展状态的操作数很大(如3000)时,单向BFS的时/空复杂度是指数级增长的cn,这时拆分成双向BFS,可以很可观的加快速度.
比较朴素BFS和双向BFS
朴素BFS
  1. 朴素BFS首先需要一个队列,用以储存当前层的状态。(当然之后的状态最终也要存进来)
  2. 需要一个set来储存已经搜索过的状态,避免重复搜索。
  3. 步数的储存:
    • 可以将第二步的set转化为dict,给每个状态记录最小步数,需要一点额外空间即可(O(n))。
    • 可以在状态结构体里增加step字段,生成状态时记录,同样是O(n) 空间。
    • 可以裸层先,用全局step,每次处理q都遍历完一层,然后step+1.
  4. 退出条件:
    • 队列为空,表示已经搜完所有路径。
    • 检测生成的状态是目标。
  5. 答案:就是记录的当前层step。
双向BFS
  1. 双向BFS需要两个队列,储存两个对向搜索方向的状态。
  2. 需要两个set来储存两个方向上搜索过的状态。
  3. 步数的储存:
    • 基本上用dict存最便于写代码,另外两种也可以,麻烦一点。
  4. 退出条件:
    • 任意队列为空,表示有一方搜完路径,依然无法交汇。
    • 生成的状态在对方的已访问列表里。
  5. 答案:当前方向的step1+对向访问列表里这个状态的step2。
  6. 补充:每次选择两个方向中更小的那个队列进行搜索,显然需要搜的状态数会少一些。

2. 复杂度分析

  1. 最坏复杂度不变,和朴素BFS相同,甚至当无解时,实际上做了两遍BFS,耗费是乘2的, O(c^n)
  2. 当有解时,会提升很多,虽然还是O(c^n)

3. 常见应用

  1. 状态转化的最小步数搜索。

4. 常用优化

  1. 方向选择:从少的队列向多的队列扩展。
  2. 代码书写方式:两个搜索,可以设置一个函数,通过传参的方式确定方向,大部分代码是复用的。选择队列,对调visited的位置即可。
  3. 步数可以用两个常量储存,层先法模式处理;或者visited用字典,多点空间。
  4. 优化建图。

二、 模板代码

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

三、其他

  1. 双向BFS是对单向BFS的时空优化,确实不如朴素bfs简单,遇到故意卡的题再用吧。

四、更多例题

  • 773. 滑动谜题 没什么区别,状态自己序列化成字符串或者二进制即可。

五、参考链接

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值