对于一些问题我们使用普通BFS求解时,队列中最多会存在两层的搜索节点,搜索空间的上界取决于目标节点所在的搜索层次的深度所对应的宽度,一旦层数比较多,那可能会出现搜索爆炸的问题,双向BFS可以让我们不使用这么宽的搜索空间,这可以保证搜索到目标结果
1.双向BFS
同时从两个方向开始搜索,一旦搜索到相同的值,意味着找到了一条连通起点和终点的最短路径。
这类问题的要求通常是有解,在一定范围内求最短路径,这种情况双向BFS的搜索空间只有普通BFS的空间消耗的几百分之一。
基本实现思路
1.创建两个队列用于两个方向的搜索
2.创建两个哈希表用于判重和记录步数
3.为了尽可能减少搜索的次数,每次从队列中取值进行扩展时,先判断哪个队列容量较小
4.如果在搜索的过程中搜到了对方搜索过的节点,说明找到了最短路径
伪代码
qa、qb为两个方向的队列
da、db为两个方向的哈希表
//从队列中取出一个元素进一次完整扩展的逻辑
void extend(queue q,map cur,map other){}
//两个队列都不为空继续搜索
//如果一个队列空了,停止搜索,说明从某个方向搜到底也找不到终点
while(条件)
{
if(qa.size()<qb.size())extend(qa,da,db);
else extend(qb,db,da);
}
2.题目
2.1 字串变换
这道题求10步以内的最少变换步数,最少变换步数我们想到BFS,10步以内是在一定范围内,我们就要使用双向BFS了,代码如下
#include <iostream>
#include <queue>
#include <unordered_map>
using namespace std;
const int N=7;
int n;
string A,B,a[N],b[N];
int extend(queue<string>&q, unordered_map<string,int>&da,
unordered_map<string,int>&db, string a[], string b[]){
int m=q.size();
while(m--)
{ //每次扩展一层
auto f=q.front(); q.pop(); //父串出队
for(int i=0; i<n; i++) //枚举规则
for(int j=0; j<f.size(); j++) //枚举父串中的位置
if(f.substr(j,a[i].size())==a[i]){
string s=f.substr(0,j)+b[i]+f.substr(j+a[i].size());//新串,substr参数左闭右开
if(da.count(s)) continue;
if(db.count(s)) return da[f]+db[s]+1;
da[s]=da[f]+1; //变换步数
q.push(s); //子串入队
}
}
return 11;
}
int bfs(){
if(A==B) return 0;
queue<string> qa,qb;//两个方向的队列
unordered_map<string,int> da,db;//两个方向的哈希表
qa.push(A),qb.push(B); da[A]=db[B]=0;
int step=10, t;
while(step--){//十步之内
if(qa.size()<=qb.size()) t=extend(qa,da,db,a,b);
else t=extend(qb,db,da,b,a);
if(t<=10) return t;
}
return 11;//十步之内没有找到返回11
}
int main(){
cin>>A>>B;
while(cin>>a[n]>>b[n]) n++;//输入规则并记录规则个数
int t=bfs();
t==11?printf("NO ANSWER!"):printf("%d",t);
}
2.2 Nightmare ||
使用双向BFS,建立两个队列,分别从男孩的初始位置、女孩的初始位置开始搜索,两边轮流进行,两个人双向奔赴。在每一秒中,男孩这边BFS三层,女孩这边BFS一层,使用数组vis记录每个位置对于男孩和女孩的可达性。在BFS的每次扩展时,要实时计算新位置与鬼之间的曼哈顿距离,如果已经小于当前秒数的2倍,那么这个位置不合法,不再入队。在BFS的过程中,第一次出现某个位置既能被男孩到达,也能被女孩到达时,当前秒数就是两人会合的最短时间。
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#define x first
#define y second
using namespace std;
const int N=810;
int n,m;
char g[N][N]; //地图
int vis[N][N]; //2表示女孩走过,1表示男孩走过,0都没走过
pair<int,int> boy,girl,ghost[2]; //存储人、鬼的初始位置
int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1}; //搜索的方向数组
bool check(int x,int y,int tim){
if(x<0||x>=n||y<0||y>=m||g[x][y]=='X') return false;
for(int i=0;i<2;i++)
if(abs(x-ghost[i].x)+abs(y-ghost[i].y)<=tim*2)return false;
return true; //(x,y)合法则返回true
}
int bfs(){
int tim=0;
memset(vis,0,sizeof vis);
queue<pair<int,int>> qb,qg;
qb.push(boy); qg.push(girl);
while(qb.size()||qg.size()){
tim++; //增加1秒
for(int i=0;i<3;i++) //男孩走3步
for(int j=0,s=qb.size();j<s;j++){ //枚举队中所有点
pair<int,int> t=qb.front(); qb.pop();
int x=t.x, y=t.y;
if(!check(x,y,tim)) continue; //(x,y)非法则跳过
for(int k=0;k<4;k++){ //4个方向
int a=x+dx[k], b=y+dy[k];
if(check(a,b,tim)){ //(a,b)合法
if(vis[a][b]==2) return tim; //2表示女孩走过
if(!vis[a][b]) vis[a][b]=1,qb.push({a,b});
}
}
}
for(int i=0;i<1;i++) //女孩走1步
for(int j=0,s=qg.size();j<s;j++){
pair<int,int> t=qg.front();qg.pop();
int x=t.x, y=t.y;
if(!check(x,y,tim)) continue;
for(int k=0;k<4;k++){
int a=x+dx[k], b=y+dy[k];
if(check(a,b,tim)){
if(vis[a][b]==1) return tim; //1表示男孩走过
if(!vis[a][b]) vis[a][b]=2,qg.push({a,b});
}
}
}
}
return -1; //无解返回-1
}
int main(){
int T;scanf("%d",&T);
while(T--){
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++) scanf("%s",g[i]);
for(int i=0,t=0;i<n;i++) //找出人、鬼的位置
for(int j=0;j<m;j++)
if(g[i][j]=='M') boy={i,j};
else if(g[i][j]=='G') girl={i,j};
else if(g[i][j]=='Z') ghost[t++]={i,j};
printf("%d\n",bfs());
}
}