目录
一、前言
二、BFS原理
二、BFS与最短路径
1、最短路径问题用BFS
2、迷宫(2019年省赛,填空题,lanqiaoOJ题号602)
(1)字典序最小的最短路径
(2)输出路径的两种方法
三、BFS与连通性判断
1、全球变暖(2018年省赛,lanqiaoOJ题号178)
(1)queue
(2)list
(3)deque
2、剪邮票(2016年省赛,填空题,lanqiaoOJ题号1505)
一、前言
本文主要讲了BFS的原理、BFS与最短路径、BFS与连通性判断以及相应的例题。
二、BFS原理
BFS搜索的原理:“逐层扩散”。从起点出发,按层次从近到远,逐层先后搜索。
编码:用队列实现。
应用:BFS 一般用于求最短路径问题,BFS 的特点是逐层搜索,先搜到的层离起点更近。
【比如:找从@到*的最短路径】
【用队列实现】
二、BFS与最短路径
1、最短路径问题用BFS
BFS的特点:逐层扩散。
- 往 BFS 的队列中加入邻居结点时,按距离起点远近的顺序加入:先加入距离起点为 1 的邻居结点,加完之后,再加入距离为 2 的邻居结点,等等
- 搜完一层,才会继续搜下一层。
最短路径:从起点开始,沿着每一层逐步往外走,每多一层,路径长度就增加 1。
所有长度相同的最短路径都是从相同的层次扩散出去的。
搜到第一个到达终点的路径,就是最短路径。
【应用场合】
点和点直接的距离是1,即边长是1。
2、迷宫(2019年省赛,填空题,lanqiaoOJ题号602)
【题目描述】
下图给出了一个迷宫的平面图,其中标记为 1 的为障碍,标记为 0 的为可以通行的地方。
010000
000100
001001
110000
迷宫的入口为左上角,出口为右下角,在迷宫中,只能从一个位置走到这个它的上、下、左、右四个方向之一。对于上面的迷宫,从入口开始,可以按 DRRURRDDDR 的顺序通过迷宫,一共 10 步。其中 D、U、L、R 分别表示向下、向上、向左、向右走。对于下面这个更复杂的迷宫 (30行50列),请找出一种通过迷宫的方式,其使用的步数最少,在步数最少的前提下,请找出字典序最小的一个作为答案。请注意在字典序中 D<L<R<U。
(1)字典序最小的最短路径
- 题目求字典序最小的最短路径
- 在每次扩散下一层 (往 BFS 的队列中加入下一层的结点) 时,按字典序 "D<L<R<U" 的顺序加下一层的结点,那么第一个搜到的最短路径就是字典序最小的。
- 计算复杂度:每个点只搜一次,即进入队列和出队列一次。复杂度 O(n),n 是迷宫内结点的总数。
- BFS 能用于解决1千万个点的最短路问题。
(2)输出路径的两种方法
【简单方法】
- 每扩展到一个点 v,都在 v 上存储从起点 s 到 v 的完整路径。
- 到达终点 t 时,得到了从起点 s 到 t 的完整路径。
优点:简单、适合小图。
缺点:占用大量空间,因为每个点上都存储了完整的路径。不适合大图。
from queue import Queue
mp=[]
for i in range(0,30):
mp.append(input()) #读迷宫
for i in range(len(mp)):
mp[i]='1'+mp[i]+'1' #为迷宫加左边和右边的围墙
mp=[52*'1']+mp+[52*'1'] #为迷宫加上面和下面的围墙
#print([52*'1'])
#print(mp) mp现在是一个只含有字符串的列表
vis=[list(map(int,list(i))) for i in mp] #记录迷宫的状态,vis是一个二维数组
#print(vis)
k=('D','L','R','U') #方向
dir=((1,0),(0,-1),(0,1),(-1,0))
vis[1][1]=1 #起点是(1,1),终点是(30,50)
q=Queue() #BFS: 队列实现
q.put((1,1,"")) #队列: 坐标x、坐标y、路径
while q.qsize()!=0: #以(1,1)为起点开始移动
x,y,p=q.get()
if x==30 and y==50:
print(p)
exit #打印完整路径,退出
for i in range(4):
nx=x+dir[i][0]
ny=y+dir[i][1]
if vis[nx][ny]!=1: #把访问过的点变成墙,后面不再访问
vis[nx][ny]=1
path=p+k[i] #记录从起点到这个点的完整路径
q.put((nx,ny,path))
【标准方法】
- 在每个点上记录它的前驱点
- 从终点一步步回溯到起点,得到一条完整路径。
优点:节省空间,因为每个点上只存储了上一个点。适合大图。
(在DFS中,路径可以用栈来记录,参考 19 讲 “路径之谜,lanqiaoOJ题号89” )
from queue import Queue
def print_path(x,y):
if x==1 and y==1:
return #回溯到了起点,递归结束,返回
if pre[x][y]=='D':
print_path(x-1,y) #回溯,往上 U
if pre[x][y]=='L':
print_path(x,y+1) #回溯,往右 R
if pre[x][y]=='R':
print_path(x,y-1) #回溯,往左 L
if pre[x][y]=='U':
print_path(x+1,y)
print(pre[x][y],end="")
mp=[]
for i in range(0,30):
mp.append(input()) #读迷宫
for i in range(len(mp)):
mp[i]='1'+mp[i]+'1' #为迷宫加左边和右边的围墙
mp=[52*'1']+mp+[52*'1'] #为迷宫加上面和下面的围墙
vis=[list(map(int,list(i))) for i in mp] #记录迷宫的状态
k=('D','L','R','U') #方向
dir=((1,0),(0,-1),(0,1),(-1,0))
pre=[[(-1,-1)]*(52) for i in range(32)] #用于保存前一个点
#下面是bfs
vis[1][1]=1 #起点是(1,1),经过了这一点,就把这一点变成围墙
q=Queue()
q.put((1,1))
while q.qsize()!=0: #以(1,1)为起点开始移动
x,y=q.get()
if x==30 and y==50:
print_path(30,50)
exit #打印路径,退出
for i in range(4): #BFS: 队列实现
nx=x+dir[i][0]
ny=y+dir[i][1]
if vis[nx][ny]!=1: #把访问过的点变成墙,后面不再访问
vis[nx][ny]=1
pre[nx][ny]=k[i]
q.put((nx,ny))
三、BFS与连通性判断
连通性判断:
图论的一个简单问题,给定一张图,图由点和连接点的边组成,要求找到图中互相连通的部分。(在DFS中,我们也讲解了连通性判断)
1、全球变暖(2018年省赛,lanqiaoOJ题号178)
【题目描述】
你有一张某海域 NxN 像素的照片,"." 表示海洋、"#" 表示陆地,如下所示:
其中 "上下左右" 四个方向上连在一起的一片陆地组成一座岛屿。例如上图就有 2 座岛屿。由于全球变暖导致了海面上升,科学家预测未来几十年,岛屿边缘一个像素的范围会被海水淹没。具体来说如果一块陆地像素与海洋相邻 (上下左右四个相邻像素中有海洋),它就会被淹没。例如上图中的海域未来会变成如下样子:
请你计算:照片中有多少岛屿会被完全淹没。照片保证第 1 行、第 1 列、第 N 行、第 N 列的像素都是海洋。
【输入描述】
第一行包含一个整数 N (1<N<1000)。以下 N 行 N 列代表一张海域照片。
【输出描述】
输出一个整数表示答案。
【BFS判断连通性的步骤】
- 从图上任意一个点 u 开始遍历,把它放进队列中。
- 弹出队首 u,标记 u 已搜过,然后搜索 u 的邻居点,即与 u 连通的点,放到队列中。
- 继续弹出队首,标记搜过,然后搜索与它连通的邻居点,放进队列。
继续以上步骤,直到队列为空,此时一个连通块已经找到。
其他没有访问到的点,属于另外的连通块,按以上步骤再次处理这些点。
最后所有点都搜到,所有连通块也都找到。
【什么岛屿不会被完全淹没】
- 若岛中有个陆地(称为高地),它周围都是陆地,那么这个岛不会被完全淹没。
- 用 BFS 搜出有多少个岛(连通块),检查这个岛有没有高地,统计那些没有高地的岛 (连通块) 的数量,就是答案。
- 计算复杂度:每个像素点只用搜一次且必须至少搜一次,共 N^2 个点,BFS 的复杂度是 O(N^2),不可能更好了。
【BFS的三种实现】
- queue
- list
- deque。 deque最快
用 “全球变暖” 这题演示三种实现
(1)queue
from queue import *
def bfs(x,y):
global flag
q=Queue()
q.put((x,y))
vis[x][y]=1
while not q.empty():
x,y=q.get()
if mp[x][y+1]=='#' and mp[x][y-1]=='#' and mp[x+1][y]=='#' and mp[x-1][y]=='#':
flag=1
for u,v in ((0,1),(0,-1),(1,0),(-1,0)): #拓展四个方向
nx=x+u
ny=y+v
if vis[nx][ny]==0 and mp[nx][ny]=="#":
q.put((nx,ny))
vis[nx][ny]=1
n=int(input())
mp=[]
for i in range(n):
mp.append(list(input()))
vis=[]
for i in range(n):
vis.append([0]*n)
ans=0
for i in range(n):
for j in range(n):
if vis[i][j]==0 and mp[i][j]=="#": #没被访问过且是陆地
flag=0
bfs(i,j)
if flag==0:
ans+=1
print(ans)
(2)list
def bfs(x,y):
global flag
q=[(x,y)] #用list实现队列
vis[x][y]=1
while q:
x,y=q.pop(0)
if mp[x][y+1]=='#' and mp[x][y-1]=='#' and mp[x+1][y]=='#' and mp[x-1][y]=='#':
flag=1
for u,v in ((0,1),(0,-1),(1,0),(-1,0)): #拓展四个方向
nx=x+u
ny=y+v
if vis[nx][ny]==0 and mp[nx][ny]=="#":
q.append((nx,ny))
vis[nx][ny]=1
n=int(input())
mp=[]
for i in range(n):
mp.append(list(input()))
vis=[]
for i in range(n):
vis.append([0]*n)
ans=0
for i in range(n):
for j in range(n):
if vis[i][j]==0 and mp[i][j]=="#": #没被访问过且是陆地
flag=0
bfs(i,j)
if flag==0:
ans+=1
print(ans)
(3)deque
deque 最快,建议需要用队列时,用 deque
from collections import *
def bfs(x,y):
global flag
q=deque()
q.append((x,y))
vis[x][y]=1
while q:
x,y=q.popleft()
if mp[x][y+1]=='#' and mp[x][y-1]=='#' and mp[x+1][y]=='#' and mp[x-1][y]=='#':
flag=1
for u,v in ((0,1),(0,-1),(1,0),(-1,0)): #拓展四个方向
nx=x+u
ny=y+v
if vis[nx][ny]==0 and mp[nx][ny]=="#":
q.append((nx,ny))
vis[nx][ny]=1
n=int(input())
mp=[]
for i in range(n):
mp.append(list(input()))
vis=[]
for i in range(n):
vis.append([0]*n)
ans=0
for i in range(n):
for j in range(n):
if vis[i][j]==0 and mp[i][j]=="#": #没被访问过且是陆地
flag=0
bfs(i,j)
if flag==0:
ans+=1
print(ans)
2、剪邮票(2016年省赛,填空题,lanqiaoOJ题号1505)
【题目描述】
有 12 张连在一起的 12 生肖的邮票。现在要从中剪下 5 张来,要求必须是连着的。仅仅连接一个角不算相连。比如图中,粉红色所示部分是合格的剪取。
请你计算一共有多少种不同的剪取方法。
【思路:暴力求全排列+检查连通性】
(1)用递归暴力列出所有可能的排列:从 12 个数中选 5 个数进行全排列。
(2)判断这 5 个数是否连通。
算法复杂度:12*11*10*9*8=12!/7!=95040
可行!
【判断 2 个数是否连通】
小技巧:
- 在原图中向上为 -4,向下为 +4,向左为 -1,向右为 +1,但是遇到 34578 这种 4+1=5,这种情况不符合,所以重构一下原图。
- 向上为 -5,向下为 +5,向左为 -1,向右为 +1。
判断图中的 {2,3,4,8,9} 是否连通。用队列:
- 2 进队列:当前队列是(2);
- 2 的邻居进(push)队列:当前队列是(2 3);
- 弹出(pop)2:当前队列是(3);
- 3 的邻居进队列:当前队列是(3 4 8);
- 弹出 3;当前队列是(4 8)
- 4 的邻居进队列:当前队列是(4 8 9)
- 弹出 4:当前队列是(8 9)
- 8没有没处理过的邻居了。
- 弹出 8:当前队列是(9)
- 弹出9;
- 队列空。
如果5个数都进过队列,它们就是连通的。
from queue import *
def bfs(): #a[0]~a[4]这前5个数是递归出来的5个数。用BFS判断它们是否连通
vis=[0]*5 #这5个数的状态,判断其中某个数是否已经用队列处理过
p=0 #进队列的个数。如果5个数都进过队列,说明这5个数连通
q=Queue()
q.put(0) #第一个进队列的数
vis[0]=1 #表示0用队列处理过了
while not q.empty():
i=q.get() #得到队列的第一个数
p+=1
for j in range(5):
if vis[j]==0: #j没有用队列处理过
for k in (-1,1,-5,5): #k是上下左右4个方向
if a[i]+k==a[j]: #与j在k方向连接
q.put(j)
vis[j]=1 #进队列了
if p==5:
return True
else:
return False
def perm(s,t):
global num
if s==5:
if bfs()==True:
num+=1 #得到一个5个数的排列,用bfs判断5个数是否连通
else:
for i in range(s,t+1):
a[s],a[i]=a[i],a[s] #交换
perm(s+1,t)
a[i],a[s]=a[s],a[i]
a=[1,2,3,4,6,7,8,9,11,12,13,14] #不包括5, 10
num=0 #统计排列数
perm(0,11) #求从第0个数到第11个数的全排列
print(num//120) #除以120得到组合数
以上,BFS的入门与应用
祝好