今天讲一下进阶版的bfs:
目录
背景:正常跑bfs时,当分支很多的时候容易超时,今天讲一下双向bfs,从两个方向开始走,这样的话分支越多,它的优势就越明显。最好可以达到开一个方的效果。但是它仅限于终点确定的题。
话不多说,先上模板-dbfs
//dbfs模板 (双队列)
qa.push(s);qb.push(n);
while(!qa.empty()&&!qb.empty()){
int la=qa.size(),lb=qb.size();
if(la<=lb){//这个if和else是dbfs的精髓,没有这个就不是dbfs
while(la--){
int cur=qa.front();qa.pop();
for(枚举所有情况){
if(越界) continue;
if(结束) return ;
if(自我重复)(恢复),continue;
标记,push(v);(回溯)
}
}
}
else{…………}//和上面一样
}
看完了模板那就直接上题了。
题目:八数码难题
思路1:
首先巩固一下:双向bfs模板 (双队列)
1,起点终点分别入队,标记vis,然后进入非空循环
2,对最少队伍处理一层:保存,出队,处理所有状态 (层数就是步数,也是时间…………)
3,判断越界,自我重复等,判断结束条件,更新vis,入队,回溯然后我们就可以对这个题进行正式分析了。
首先你可能想到了状态压缩,但是好像这个题不太适合这个想法吧,毕竟还要对状态进行修改的。那么,我们用数或字符串来存状态都是个很聪明的决定(只要不是数组就行),无论是“ 标记数码是否一致 ”还是“ 保存数码状态 ”都很快!
操作:对每个字符状态向四个方向变化,然后入队和标记即可。
#include<bits/stdc++.h> //八数码难题P1379(双向bfs)双队列写法(很好写)
using namespace std;
typedef long long ll;
string now,cur,s,g="123804765";//终点状态
int ans,dx[4]={1,-1,0,0},dy[4]={0,0,1,-1};
queue<string> qa,qb;
unordered_map<string,int> v; //标记数组:状态与访问的映射:0表示未访问过,1表示顺序访问过,2表示逆序访问过
void solve()
{
if(s==g) { printf("0"); exit(0); }
qa.push(s); qb.push(g);//起始状态与终止状态同时入队
v[s]=1; v[g]=2;//标记访问过
while(!qa.empty()&&!qb.empty()){
ans++; //按层遍历,队伍一次走一层
int na=qa.size(),nb=qb.size();
if(na<=nb){
while(na--){
string cur=qa.front();qa.pop();//保存,出队
int k=cur.find('0');
int nx,ny,x=k/3,y=k%3;//转化成坐标
for(int i=0;i<4;i++){
nx=x+dx[i];ny=y+dy[i];
if(nx<0 || nx>=3 || ny<0 || ny>=3) continue;//判断越界
swap(cur[k],cur[nx*3+ny]);//交换位置就自动获得了新的状态
if(v[cur]==2) {//判断结束
cout<<ans;exit(0);
}
if(v[cur]==1) {//判断自我重复
swap(cur[k],cur[nx*3+ny]);
continue;
}
v[cur]=1;//标记
qa.push(cur);//入队
swap(cur[k],cur[nx*3+ny]);//回溯,因为还要别的方向要走呢
}
}
}
else{
while(nb--){
string cur=qb.front();qb.pop();
int k=cur.find('0');
int x=k/3,y=k%3;
for(int i=0;i<4;i++){
int nx=x+dx[i],ny=y+dy[i];
if(nx<0 || nx>=3 || ny<0 || ny>=3) continue;
swap(cur[k],cur[nx*3+ny]);
if(v[cur]==1) {
cout<<ans;exit(0);
}
if(v[cur]==2) {
swap(cur[k],cur[nx*3+ny]);
continue;
}
v[cur]=2;
qb.push(cur);
swap(cur[k],cur[nx*3+ny]);
}
}
}
}
}
int main()
{ cin>>s;
solve();
}
当然还有一个版本,比较简洁,不过要多几步操作。
思路2:
dbfs()模板:(单队列)
1,起始和终点状态入队,标记来过,记录步数(v为1和2,ans为0和1)
2,保存,出队,处理所有状态:now<-last
3,判断越界,判断重复(正向重复,反向重复),判断结束(正向和反向相遇),更新步数,更新vis,入队,回溯
->判重:v[now]=v[last],你不知道此时是正向的还是反向的,所以只能和上个状态last比较
->结束:v[now]+v[last]=?说明相遇了
->更新步数,方向:ans[now]=ans[last]+1,v[now]=v[last],是last方向走过的点
#include<bits/stdc++.h> //单队列
#define ll long long int
using namespace std;
string now,last,s,g="123804765";
int dx[4]={1,-1,0,0},dy[4]={0,0,1,-1};
queue<string> q;
unordered_map<string,int> v; //标记数组:状态与访问的映射:0表示未访问过,1表示顺序访问过,2表示逆序访问过
unordered_map<string,int> ans;//记录步数:状态和步数的映射
void solve()
{
if(s==g) { printf("0"); exit(0);}
q.push(s); q.push(g);//起始状态与终止状态同时入队
ans[s]=0; ans[g]=1;//g为1是因为终点这步是要走的,而起点为0是因为不需要走
v[s]=1; v[g]=2;//标记访问过
while(!q.empty())
{
last=q.front(); q.pop(); now=last;//now存放交换后状态,cur存放交换前状态
int k=last.find('0');
int nx,ny,x=k/3,y=k%3;
for(int i=0;i<4;i++) //四个循环要用用一个cur地图
{
nx=x+dx[i]; ny=y+dy[i];
if(nx<0 || nx>=3 || ny<0 || ny>=3) continue;
swap(now[k],now[nx*3+ny]);
if(v[now]==v[last]) //判重(now可能之前走过,即环路,可能是顺序环路或逆序环路但绝不会对面方向来过)
{
swap(now[k],now[nx*3+ny]); //还原地图并跳过
continue;
}
if(v[now]+v[last]==3) //结束状态:找到了已被另一方向访问过的点(所以不会被重复标记)
{
printf("%d",ans[last]+ans[now]);
exit(0);
}
ans[now]=ans[last]+1; //更新步数
v[now]=v[last]; //更新状态:与上一状态的方向保持一致
q.push(now); //入队
swap(now[k],now[nx*3+ny]); //回溯,还原地图为下个方向的循环使用
}
}
}
int main()
{ cin>>s;
solve();
}
好了。这个题怎么样?是不是有点点难?
没事再来一道类型题,今天争取拿下这种题型
题目:噩梦
思路:
还是一道裸的dbfs,女孩和男孩要同时走,但是要注意鬼是先走2步,其实完全不用标记鬼走过的地方,因为根据鬼的起始位置和时间即可推出你目前的决策点是否可走!然后bfs就可以了。
#include <bits/stdc++.h>//两个鬼一个男孩一个女孩。每一秒鬼先走(可穿墙)2步,然后男孩3步和女孩1步(不能走墙和鬼走过的) 问最终男女相遇最短时间
using namespace std;//. X M G Z表示“道路 墙 男孩 女孩 鬼”
typedef pair<int,int>pi;//要存下(x,y)坐标
const int N=810;
int n,m,vis[N][N],dx[]={-1,0,1,0},dy[]={0,1,0,-1};;//男孩走过是1,女孩是2,未走是0
char g[N][N];
queue<pi>qb,qg;//两个队列的dbfs
pi ghost[2],boy,girl;//坐标
bool check(int x,int y,int step){ //处理曼哈顿距离:你的决策点和时间
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].first)+abs(y-ghost[i].second)<=step*2)return false;
}
return true;
}
int dbfs(){
int cnt=0;
memset(vis,0,sizeof(vis));
for(int i=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[cnt++]={i,j};
}
}
int time=0;//扩展次数,或时间数
qb.push(boy),qg.push(girl);
while(!qb.empty()&&!qg.empty()){
time++;
for(int i=0;i<3;i++)//走三步,就是扩3层
for(int j=0,len=qb.size();j<len;j++){
pi t=qb.front();qb.pop();
int x=t.first,y=t.second;
if(!check(x,y,time)) continue;//这是个坑!上一秒还安全的点,(因为鬼先移动)这一秒鬼先来了,那就不能走
for(int k=0;k<4;k++){
int a=x+dx[k],b=y+dy[k];
if(check(a,b,time)){
if(vis[a][b]==2)return time;//判断对面来了,那就相遇了
if(!vis[a][b]){//自我不重复
vis[a][b]=1;
qb.push({a,b});
}
}
}
}
for(int i=0;i<1;i++)
for(int j=0,len=qg.size();j<len;j++){
pi t=qg.front();qg.pop();
int x=t.first,y=t.second;
if(!check(x,y,time)) continue;
for(int k=0;k<4;k++){
int a=x+dx[k],b=y+dy[k];
if(check(a,b,time)){
if(vis[a][b]==1)return time;
if(!vis[a][b]){
vis[a][b]=2;
qg.push({a,b});
}
}
}
}
}
return -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]);
printf("%d\n",dbfs());
}
return 0;
}
行了,以上就是今天的内容,有点难度的还是。好好消化!