【算法每日一练]-双向bfs 篇6 八数码难题(两个解法),噩梦

今天讲一下进阶版的bfs:

目录

先上模板-dbfs

题目:八数码难题

  思路1: 

  思路2:

题目:噩梦

  思路: 


        

          

背景:正常跑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;
}

行了,以上就是今天的内容,有点难度的还是。好好消化!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值