BFS判重和双向广搜

目录

一、前言

二、BFS判重

1、set 判重

2、字典判重

3、跳蚱蜢(2017年省赛,lanqiaoOJ题号642)

(1)字典去重、用 list 实现队列

(2)set() 去重、用 list 实现队列

(3)set() 去重、用 deque 实现队列

三、双向广搜


一、前言

本文主要讲了BFS如何判重和双向广搜。

二、BFS判重

  • BFS = 队列
  • BFS:逐步扩展下一层,把扩展出的下一层状态放进队列中处理。
  • 如果这些状态有相同的,只需搜一次,只需要进入队列一次。
  • 必须判重。

Python 判重方法:set、字典

1、set 判重

  • set() 函数创建一个无序、不重复元素集
  • 关系测试,删除重复数据,计算交集、差集、并集、补集。
a=set()
a.add("678");print(a)
a.add("123");print(a)
a.add("678")    #第二个"678"加不进集合了
print(a)
b=sorted(a);print(b);print()
s=set("aabc");print(s);print()  #a,b,c的输出顺序不定的,输出 {'a','b','c'}
s=set(["aabc","bca"]);print(s);print()      #输出{"aabc","bca"}

b=set()
b.add(678);print(b)
b.add(123);print(b)
b.add(678);print(b);print()

a=set([1,1,2,3,5]);print(a)
b=set([1,2,3,4]);print(b)
print(a-b)  #差,在集合a中但不在集合b中的元素
print(b-a)  #差
print(a&b)  #交,同时在集合a和b中的共同元素
print(a|b)  #并,包括集合a和b中所有元素

2、字典判重

  • 字典:无序、可变、有索引的集合。
  • 字典:用花括号定义,有键和值。
a={5:"xy"}
a[1]="cb"
a[2]="kd"
a[2]="af"
print(a);print()

b=sorted(a.items());print(b)
b=sorted(a.items(),key=lambda x:x[0]);print(b)  #按键排序
b=sorted(a.items(),key=lambda x:x[1]);print(b)  #按值排序
print()

a={"food":"xy","price":555}
a[3]=899
print(a)

3、跳蚱蜢(2017年省赛,lanqiaoOJ题号642)

【题目描述】

有 9 只盘子,排成 1 个圆圈。其中 8 只盘子内装着 8 只蚱蜢,有一个是空盘。

把这些蚱蜢顺时针编号为1~8。

每只蚱蜢都可以跳到相邻的空盘中,也可以再用点力,越过一个相邻的蚱蜢跳到空盘中。请你计算一下,如果要使得蚱蜢们的队形改为按照逆时针排列,并且保持空盘的位置不变(也就是1-8换位,2-7换位,...),至少要经过多少次跳跃?

  • 从起始状态到终止状态,求最少跳跃次数
  • 最短路径问题
  • 用BFS
  • 直接让蚱蜢跳到空盘有点麻烦,因为有很多蚱蜢在跳。
  • 反过来看,让空盘跳,跳到蚱蜢的位置,简单多了,只有一个空盘在跳。

【画圆为线】

  • 题目是一个圆圈,不好处理,用一个建模技巧 “化圆为线”,把圆形转换为线形。
  • 把空盘看成 0,有 9 个数字 {0,1,2,3,4,5,6,7,8},一个圆圈上的 9 个数字,拉直成了一条线上的 9 个数字,这条线的首尾两个数字处理成相连的。
  • 八数码问题:有 9 个数字 {0,1,2,3,4,5,6,7,8),共有 9! = 362880 种排列,不算多。

【最短路径】

初始状态:“012345678”, 目标状态:“087654321”。 

从初始状态 "012345678" 跳一次,有 4 种情况:"102345678"、“210345678”、 “812345670”、 “712345608”。然后从这 4 种状态继续跳到下一种状态,一直跳到目标状态为止。

用 BFS 扩展每一层。每一层就是蚱蜢跳了一次,扩展到某一层时发现终点 “087654321”,这一层的深度就是蚱蜢跳跃的次数。

【去重】

  • 如果不去重?
  • 第 1 步到第 2 步,有 4 种跳法;第 2 步到第 3 步,有 4*4 种;...;第 20 步,有 4^20 = 1万亿种。

  • 判重:判断有没有重复跳,如果跳到一个曾经出现过的情况,就不用往下跳了。一共只有 9! =362880 种情况。
  • 代码的复杂度:在每一层,能扩展出最少 4 种、最多 362880 种情况,最后算出的答案是 20 层,那么最多算 20*362880=7,257,600 次。在代码中统计实际的计算次数,是 1451452 次。
  • 队列:最多有 9!=362880 种情况进入队列。

(1)字典去重、用 list 实现队列

速度慢:3s

def insertQueue(q:list,dir:int,news:tuple,vis):     #q是列表,vis是字典,dir是方向
    pos=news[1]         #0的位置
    status=news[0]      #字符串
    insertPos=(pos+dir+9)%9     #新的0的位置
    #将字符串转为列表比较好处理
    t=list(status)      #先转换成列表
    t[pos],t[insertPos]=t[insertPos],t[pos]
    addStatus="".join(t)    #再由列表转换为一个字符串,join完是返回一个字符串
    if addStatus not in vis:
        vis[addStatus]=1    #向字典添加,去重
        q.append((addStatus,insertPos,news[2]+1))

q=[("012345678",0,0)]   #列表,用于实现队列,比较慢
vis={"012345678":1}     #字典,用于去重
while q:
    news=q.pop(0)   #news是一个元组
    if news[0]=="087654321":     #到达了目标状态,输出最少步数
        print(news[2])
        break
    insertQueue(q,-2,news,vis)  #拓展下一层的4种情况
    insertQueue(q,-1,news,vis)
    insertQueue(q,1,news,vis)
    insertQueue(q,2,news,vis)

(2)set() 去重、用 list 实现队列

def insertQueue(q:list,dir:int,news:tuple,vis):     
    pos=news[1]         #0的位置
    status=news[0]      
    insertPos=(pos+dir+9)%9     #新的0的位置
    #将字符串转为列表比较好处理
    t=list(status)      
    t[pos],t[insertPos]=t[insertPos],t[pos]
    addStatus="".join(t)    
    if addStatus not in vis:
        vis.add(addStatus)      #判重 
        q.append((addStatus,insertPos,news[2]+1))

q=[("012345678",0,0)]   
vis=set()
vis.add("012345678")    #判重
while q:
    news=q.pop(0)  
    if news[0]=="087654321":     
        print(news[2])
        break
    insertQueue(q,-2,news,vis) 
    insertQueue(q,-1,news,vis)
    insertQueue(q,1,news,vis)
    insertQueue(q,2,news,vis)

(3)set() 去重、用 deque 实现队列

速度快:1.4s

from collections import *
def insertQueue(q:deque,dir:int,news:tuple,vis:set):     
    pos=news[1]         #0的位置
    status=news[0]      
    insertPos=(pos+dir+9)%9     #新的0的位置
    #将字符串转为列表比较好处理
    t=list(status)      
    t[pos],t[insertPos]=t[insertPos],t[pos]
    addStatus="".join(t)    
    if addStatus not in vis:
        vis.add(addStatus)      
        q.append((addStatus,insertPos,news[2]+1))   #deque的append

q=deque()
q.append(("012345678",0,0))   
vis=set()
vis.add("012345678")    #判重
while q:
    news=q.popleft()  
    if news[0]=="087654321":     
        print(news[2])
        break
    insertQueue(q,-2,news,vis) 
    insertQueue(q,-1,news,vis)
    insertQueue(q,1,news,vis)
    insertQueue(q,2,news,vis)

三、双向广搜

  • 应用场景:有确定的起点 s 和终点 t;把从起点到终点的单向搜索,变换为分别从起点出发和从终点出发的“相遇”问题。
  • 操作:从起点 s(正向搜索)和终点 t(逆向搜索)同时开始搜索,当两个搜索产生相同的一个子状态 v 时就结束,v 是相遇点。得到的 s-v-t 是一条最佳路径。
  • 队列:一般用两个队列分别处理正向 BFS 和逆向 BFS.

【双向广搜的复杂度】

当下一层扩展的状态很多时,双向广搜能大大优化,减少大量搜索

【重新思考上面的例题】

  • 队列 q1:正向搜索
  • 队列 q2:逆向搜索

from queue import *
cnt=0
meet=False
def extend(q,m1,m2):    # m1和m2是字典
    global cnt
    global meet
    s=q.get()
    for i in range(len(s)):
        if s[i]=='0':
            break
    for j in range(4):
        cnt+=1          #统计计算个数
        news=list(s)    #用list比较方便
        if j==0:
            news[(i-2+9)%9],news[i]=news[i],news[(i-2+9)%9]
        if j==1:
            news[(i-1+9)%9],news[i]=news[i],news[(i-1+9)%9]
        if j==2:
            news[(i+1+9)%9],news[i]=news[i],news[(i+1+9)%9]
        if j==3:
            news[(i+2+9)%9],news[i]=news[i],news[(i+2+9)%9]
        a="".join(news) #重新转换成字符串
        if a in m2:
            print(m1[s]+1+m2[a])
            print(cnt)  #打印计算次数
            meet=True
            return
        if a not in m1:
            q.put(a)
            m1[a]=m1[s]+1   #向字典中添加
    meet=False

q1=Queue()
q2=Queue()
q1.put("012345678")
q2.put("087654321")
mp1={'012345678':0}
mp2={'087654321':0}     #定义字典,用于判重
while not q1.empty() and not q2.empty():
    if q1.qsize()<=q2.qsize():
        extend(q1,mp1,mp2)
    else:
        extend(q2,mp2,mp1)
    if meet==True:
        break
  • 由于起点和终点的串不同,正向 BFS 和逆向 BFS 扩展的下一层数量也不同,也就是进入 2 个队列的串的数量不同,先处理较小的队列,可以加快搜索速度。
from queue import *
cnt=0
meet=False
def extend(q,m1,m2):    # m1和m2是字典
    global cnt
    global meet
    s=q.get()
    for i in range(len(s)):
        if s[i]=='0':
            break
    for j in range(4):
        cnt+=1          #统计计算个数
        news=list(s)    #用list比较方便
        if j==0:
            news[(i-2+9)%9],news[i]=news[i],news[(i-2+9)%9]
        if j==1:
            news[(i-1+9)%9],news[i]=news[i],news[(i-1+9)%9]
        if j==2:
            news[(i+1+9)%9],news[i]=news[i],news[(i+1+9)%9]
        if j==3:
            news[(i+2+9)%9],news[i]=news[i],news[(i+2+9)%9]
        a="".join(news) #重新转换成字符串
        if a in m2:
            print(m1[s]+1+m2[a])
            print(cnt)  #打印计算次数
            meet=True
            return
        if a not in m1:
            q.put(a)
            m1[a]=m1[s]+1   #向字典中添加
    meet=False

q1=Queue()
q2=Queue()
q1.put("012345678")
q2.put("087654321")
mp1={'012345678':0}
mp2={'087654321':0}     #定义字典,用于判重
while not q1.empty() and not q2.empty():
    if q1.qsize()<=q2.qsize():
        extend(q1,mp1,mp2)
    else:
        extend(q2,mp2,mp1)
    if meet==True:
        break

用 cnt 统计运行了多少次: 54568次。

前面用普通BFS计算: 1451452次

双向广搜的计算量只有 4%

  • 为什么能优化这么多?
  • 在普通 BFS 中,如果不判重,到第 20 层扩展了 4^20 种状态。在双向广搜中,假设在第 10 层相遇,正向搜索和逆向搜索在第 10 层扩展的状态数量都是4^10
  • 从4^20到4^10,得到了极大优化。

以上,BFS判重和双向广搜

祝好

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吕飞雨的头发不能秃

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值