SNOI省选模拟赛Round5 T2 逃离迷宫escape 搜索+并查集+预处理剪枝+双联通分量?

本文详细介绍了如何解决一道关于地图迷宫问题的竞赛题目,通过预处理剪枝和双联通分量优化n^4的搜索算法。利用BFS+BFS策略,结合并查集与Tarjan算法,实现复杂问题的高效求解。同时强调了思考题解优化、连通性判断和细节处理在OIer中的重要性。
摘要由CSDN通过智能技术生成

题目大意:有一个n*m的地图,有些地方是墙,有些地方是空地,有一个人和一个箱子还有一个终点,给出人,箱子,和终点的位置,求人把箱子推到终点,箱子的最少移动次数。

n,m<=1000。

【前言】

考场上只打出了n^4的搜索...虽然加了剪枝但用处不大,考完后发现std的代码可读性几乎为零,只好一个人磕了一下午....

【题解】

这题正解真是好难啊,能想到那么多。

n^4的搜索很好打,bfs+bfs即可。

但是每次箱子移动位置时,我们都要重新bfs一遍人的位置导致浪费大量时间,所以应该想到要预处理一些信息。

注意:前方高能讲解比较鬼畜,请耐心的看完。

我们把矩阵读在一行字符串里,因为这样标号很方便。

先来定义一些数组吧。

dir[i][j],表示在第i个点,去第j个方向是否越界,j是0~4之间的整数,表示4个方向。

u[4][2],矩阵用的方向数组。

delta[4],将矩阵转成一行后用的方向数组。

vis[i][j],表示第i个点能否去j方向的点。

check[i][j1][j2],表示在第i个点,从j1方向转移来,下一步向j2方向转移,这个方案是否能行。

那么,处理出check数组就是我们的最终目标,因为有了check数组我们就可以不用枚举很多不可行的状态。

dir数组很好处理,就不说了。那么我们先处理出dir数组。

然后就是处理check数组了。

首先要维护连通性,维护连通性我们能够想到并查集(但我考场上否定了),但存在一个问题,有可能一些本身联通的点,因为推箱子导致不连通。(就是这个原因,我否定了并查集的做法)。

那么,如果有一个图,使得没有一个点去掉会让图不连通,也就是双联通分量

因此采用Tarjan算法,对图进行dfs,并记录每个点的dfn和low。

在定义一个数组d,表示一个点有几个方向被访问过了,如果一个点的四个方向都被访问过了,说明这个点的答案已经统计完毕了,更新答案。

根据dfn和low的定义,我们可以判断出答案是在前面的dfs时已经统计出来了,也就是被包含在已经找到的双联通分量里,还是这个双联通分量没有统计完毕,答案在后面的dfs里。

对于包含在已经统计完毕的点,我们可以枚举上一步转移来的点和下一步去的点,设这两个点为x和y,当前点为now,据此可以分成5种情况讨论:

1)x和y的dfn值都小于dfn[now],说明可以到达。

2)x和y直接连通,说明可以到达。

3)x和y的dfn值都大于dfn[now],那么只需判断这两个点是否属于这个双联通分量,判断方法是看其所在并查集的祖先的low值是否小于dfn[now],若满足则可行。

4)dfn[x]>dfn[now],dfn[y]<dfn[now],那么只看x即可。

5)dfn[x]<dfn[now],dfn[y]>dfn[now],那么只看y即可。

综上我们可以预处理出check数组。

然后我们可以先从人的位置出发开始bfs,标记所有人能到达的位置。

接着从箱子的位置统计,如果上下左右有人能到达的位置,就将其加入队列并记录方向,作为bfs的起始队列。

然后bfs,对于当前点,如果箱子要去一个位置,那么人就要在相反的方向,也就是代码中的d[head](人)与(3-d[head])(箱子)方向的关系。

然后枚举4个方向,如果从当前方向来的转移到下一个方向去的方案可行并且下一个点从这个方向来的情况没有统计,加入队列即可。

代码中有较详细注释。

代码:

#include<bits/stdc++.h>
#define maxn 10000005
using namespace std;
typedef long long LL;
int read()
{
	char c;int sum=0,f=1;c=getchar();
	while(c<'0' || c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0' && c<='9'){sum=sum*10+c-'0';c=getchar();}
	return sum*f;
}
const int u[4][2]={{0,1},{-1,0},{1,0},{0,-1}};
int n,m;
char s[maxn];
bool check[maxn][4][4],vis[maxn][4],dir[maxn][4];
int fa[maxn],rank[maxn],anc[maxn];
int delta[4],st[maxn],q[maxn];
int dfn[maxn],low[maxn],pre[maxn],d[maxn],tot;
int step[maxn];
int getfa(int x)
{
	return fa[x]==x?x:fa[x]=getfa(fa[x]);
}
void unio(int x,int y)
{
	x=getfa(x),y=getfa(y);
	if(rank[x]<rank[y])
	fa[x]=y;
	else
	fa[y]=x,rank[x]+=rank[x]==rank[y];
}
void dfs(int start)
{
	int top,now,next,i,j,x,y,son,flag;
    st[top=1]=start,d[start]=0;
    dfn[start]=low[start]=++tot;
    for(;top;)
    {
        now=st[top];
        if(dir[now][d[now]])
        {
            next=now+delta[d[now]];
            if(s[next]!='#' && !dfn[next])//访问下一个点 没越界 不是墙且没访问过 
            {
                pre[next]=now,st[++top]=next,d[next]=0;//记录下一个点由当前点转移来 
                dfn[next]=low[next]=++tot;
            }
        }
        if(d[now]>3)//如果四个方向都访问过 
        {
            for(flag=1,i=0;i<4;i++)
            if(dir[now][i])
            {
                next=now+delta[i];
                if(s[next]=='#') continue;
                if(pre[next]==now)//如果下一个点由该点转移过来 
                {
                    son++,low[now]=min(low[now],low[next]);//有儿子,用后代更新当前的low 
                    if(low[next]>=dfn[now]) flag=0;//如果下一个点能追溯到的最早节点比当前点更早 
                }
                else
                if(pre[now]!=next)//如果该点不是由下一点转移来的 
            	low[now]=min(low[now],dfn[next]);//更新low 
            }
            if(pre[now]==-1)//是第一层递归 
            if(son>1) flag=0;//如果有儿子,说明更早地被更新了答案。 
			else flag=1;
            if(!flag)//flag==0  说明答案从前面找 
            for(i=0;i<4;i++)
            for(j=0;j<4;j++)
            {
                if(!dir[now][i] || !dir[now][j]) continue;
                x=now+delta[i],y=now+delta[j];
                if(s[x]=='#' || s[y]=='#') continue;
                if(dfn[x]<dfn[now] && dfn[y]<dfn[now])//如果下一步走的点和上一步来的点都在当前点前面,说明可以 
                check[now][i][j]=true;
                if(getfa(x)==getfa(y))//如果下一步走的点和上一步来的点联通,说明可以。 
                check[now][i][j]=true;
                if(dfn[x]>dfn[now] && dfn[y]>dfn[now])//如果下一步走的点和上一步来的点都在后面 
                if(low[anc[getfa(x)]]<dfn[now])
                if(low[anc[getfa(y)]]<dfn[now])//如果都联通,说明可以 
                check[now][i][j]=true;
                if(dfn[x]>dfn[now] && low[anc[getfa(x)]]<dfn[now] && dfn[y]<dfn[now])//分开讨论 
                check[now][i][j]=true;
                if(dfn[y]>dfn[now] && low[anc[getfa(y)]]<dfn[now] && dfn[x]<dfn[now])//分开讨论 
                check[now][i][j]=true;
            }
            else//否则看后面的
            for(i=0;i<4;i++)
            for(j=0;j<4;j++)
            {
                if(!dir[now][i] || !dir[now][j]) continue;
                x=now+delta[i],y=now+delta[j];
                if(s[x]=='#' || s[y]=='#') continue;
                check[now][i][j]=true;
            }
            for(i=0;i<4;i++)
            {
                if(!dir[now][i]) continue;
                next=now+delta[i];
                if(s[next]=='#') continue;
                if(pre[next]==now)
                unio(now,next),anc[getfa(now)]=now;//并查集联通 
            }
            top--;//弹出该点 
            continue;
        }
        d[now]++;
    }
}
int main()
{
	freopen("escape.in","r",stdin);
    freopen("escape.out","w",stdout);
	int i,j,k,head,tail,next;
    n=read();m=read();
    for(i=0;i<n;i++)
    scanf("%s",s+i*m);
    delta[0]=1,delta[1]=-m;
    delta[2]=m,delta[3]=-1;
    for(i=0;i<n;i++)
    for(j=0;j<m;j++)
    for(k=0;k<4;k++)
	{
	    int t1=i+u[k][0],t2=j+u[k][1];
	    if(t1>=0 && t1<n && t2>=0 && t2<m)
	    dir[i*m+j][k]=true;
	    else
	    dir[i*m+j][k]=false;
	}
    for(i=0;i<n*m;i++)
	fa[i]=i,rank[i]=0,anc[i]=i;
    for(i=0;i<n*m;i++)
    if(!dfn[i] && s[i]!='#')
    pre[i]=-1,dfs(i);
    for(i=0;i<n*m;i++)
    if(s[i]=='@') break;
    q[1]=i;
	memset(vis,false,sizeof(vis));
	vis[i][0]=true;
    for(head=tail=1;head<=tail;head++)
    for(i=0;i<4;i++)
    {
        if(!dir[q[head]][i]) continue;
        next=q[head]+delta[i];
        if(s[next]=='#' || vis[next][0]) continue;
        vis[next][0]=true,q[++tail]=next;
    }
    for(i=0;i<n*m;i++)
    if(s[i]=='B') break;
    for(head=1,tail=0,j=0;j<4;j++)
    {
        if(!dir[i][j]) continue;
        next=i+delta[j];
        if(vis[next][0]) q[++tail]=i,d[tail]=j;
    }
    memset(vis,0,sizeof(vis));
    for(i=1;i<=tail;i++)
    vis[q[i]][d[i]]=true,step[i]=1;
    for(;head<=tail;head++)
    {
        if(!dir[q[head]][3-d[head]]) continue;//如果从另一边过来时越界,说明不行 
        next=q[head]+delta[3-d[head]];//箱子要去的地方 
        if(s[next]=='#') continue;
        if(s[next]=='+') break;
        for(i=0;i<4;i++)
        if(check[next][d[head]][i] && !vis[next][i])
        {
            q[++tail]=next,d[tail]=i;
            vis[next][i]=true,step[tail]=step[head]+1;
        }
    }
    printf("%d\n",step[head]);
    return 0;
}
时间复杂度为O(NM)

【收获】

1)一道简单的搜索题要想优化也是可以继续优化的。

2)判断连通性要联想到并查集甚至双联通分量。

3)注重细节,脑洞大开才是一名合格的OIer。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值