题解 P1979 【华容道 】

· · 题解

博客食用效果更佳

看到这道题的第一反应就是直接上bfs啦,也没有想到什么更加优秀的算法。 然后就是15分钟打了70分,有点震惊,纯暴力诶,这么多白给分嘛,太划算了,这可是D2T3诶。

切入正题:

纯暴力的bfs会超时,是因为搜索了很多无用状态:真正有用的状态其实是起点格子走的状态,但是这道题用搜索方便转移的却是空白格子,因为题目的规则是空白格子可以移动(相当于和旁边的格子交换位置)。

只有起点格子在空白格子四周起点格子才能动,所以应该是尽量让空白格子往起点格子周围跑,而不是毫无目的地乱跑,空白格子在乱跑的时候实际上拓展了很多无用节点。

那么,就先用一个bfs预处理出空白格子到达起点格子四周的距离,然后再从四周分别开始走,取最小的。

从四周开始走的时候还是不考虑暴搜(会产生多余状态),我们只管有用的状态。还是这个道理:起点格子是要依托于空白格子才能够移动的,所以状态要保持空白格子一直在起点格子四周。

那么定义状态(i,j,k)表示起点格子的坐标为(i,j),空白格子在它的k方向(也即是坐标为(i+dx[k],j+dy[k]))。

考虑转移到后继状态:

1.可以是起点格子通过空白格子进行移动(就是俩格子交换位置)

2.可以是空白格子在起点格子四周移动,可以看成转方向。如果只用上面那种转移,也就是用起点格子自己换方向,可能没那么优秀,因为换来换去要绕一绕 。而且起点格子有可能会跑到一个离终点的地方去了,更不优。而只动空白格子可能会优秀一些 因为空白格子最优(到处都是活动格子)只用走2~4步。

然后我们发现,对于多组数据,棋盘的形状是没有改变的,所以大概可以预处理一下然后再询问。

怎么预处理呢?有一个比较实用的技巧,就是把状态当成图的节点,状态之间转移的代价当做边权,然后求从一个状态到另一个状态的最小步数就是从一个点到另一个点的最短路啦(其实本来bfs也和最短路有关系(强行沾边))

所以只需要处理出一些可行状态(可以将他们编号,以便进行最短路),然后用上面的两种转移关系连边:对于第一种,边权就是1,对于第二种,边权就是两点之间的最短路(可以bfs预处理,此时的最短路相当于边权为1)。由于边权不是全为1,不能用bfs,要写最短路算法,只要不是floyd,随便写哪个都可以的。

每次询问的时候,求出空白格子不经过起点格子(如果经过,起点格子位置就变啦)到起点格子四周的最短距离(可bfs),再从那四个状态出发(可以同时压进去的,因为答案没有叠加部分,都是一直不停更新最短距离,相当于取min),跑到终点格子。终点格子的四个状态都可以,取min就是答案。

果然吧,还是暴力好写,正解调了一个下午,考试的时候写暴力真的太划算了。

暴力代码:

#include<cstdio>
#include<algorithm>
#include<vector>
#include<cstring>
#include<queue>
#include<map>
#include<iostream>
#include<cmath>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define N 35
int rd()
{
    int f=1,s=0;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
    while(c>='0'&&c<='9'){s=(s<<3)+(s<<1)+(c^48);c=getchar();}
    return f*s;
}
struct node{
    int mx,my,nx,ny;//空格的位置,初始棋子现在的位置
    int stp;//步数 
};
int n,m,q;
int mp[N][N];
int ex,ey,sx,sy,tx,ty;
queue<node>Q;
const int dx[]={1,-1,0,0},dy[]={0,0,1,-1};
bool vis[N][N][N][N];
bool check(int xx,int yy)
{
    if(xx<0||xx>n||yy<0||yy>m||mp[xx][yy]==0) return 0;
    return 1;
}
int main() 
{
    n=rd(),m=rd(),q=rd();
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            mp[i][j]=rd();
    while(q--)
    {
        ex=rd(),ey=rd(),sx=rd(),sy=rd(),tx=rd(),ty=rd();
        if(sx==tx&&sy==ty)
        {//下面村答案是在拓展节点之后存 所以这里要特判 
            puts("0");//如果取出时存答案就不用特判(好像也不用break很多层了 
            continue; 
        }
        memset(vis,0,sizeof(vis));
        bool flag=0;
        while(!Q.empty()) Q.pop();
        node s;s.mx=ex,s.my=ey,s.nx=sx,s.ny=sy,s.stp=0;
        vis[ex][ey][sx][sy]=1;
        Q.push(s);
        while(!Q.empty())
        {
            s=Q.front();Q.pop();
            for(int i=0;i<4;i++)
            {
                node nxt;
                nxt.mx=s.mx+dx[i],nxt.my=s.my+dy[i];
                if(!check(nxt.mx,nxt.my)) continue;
                if(nxt.mx==s.nx&&nxt.my==s.ny) nxt.nx=s.mx,nxt.ny=s.my;
                else nxt.nx=s.nx,nxt.ny=s.ny;
                nxt.stp=s.stp+1;
                if(nxt.nx==tx&&nxt.ny==ty)
                {
                    flag=1;
                    printf("%d\n",nxt.stp);
                    break;
                }
                if(vis[nxt.mx][nxt.my][nxt.nx][nxt.ny]) continue;
                Q.push(nxt);
                vis[nxt.mx][nxt.my][nxt.nx][nxt.ny]=1;
            }
            if(flag) break;
        }
        if(flag) continue;
        else puts("-1");
    }
    return 0;
}

Code

满分代码:

#include<cstdio>
#include<algorithm>
#include<vector>
#include<cstring>
#include<queue>
#include<map>
#include<iostream>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define N 35
int rd()
{
    int f=1,s=0;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
    while(c>='0'&&c<='9'){s=(s<<3)+(s<<1)+(c^48);c=getchar();}
    return f*s;
}
const int dx[]={1,-1,0,0},dy[]={0,0,1,-1};
//下上右左 
int n,m,q;
int mp[N][N];
int ex,ey,sx,sy,tx,ty;
int cnt[N][N][5];
bool check(int xx,int yy)
{
    if(xx<=0||xx>n||yy<=0||yy>m||mp[xx][yy]==0) return 0;
    return 1;
}
int tot;
struct node{
    int x,y,stp/*广搜的时候用 存步数*/;
};
bool vis[N][N],mark[N*N]/*spfa的标记数组*/;
vector<pair<int,int> >G[N*N];
void add(int u,int v,int w)
{
    G[u].push_back(make_pair(v,w));
}
int bfs(int ax,int ay,int bx,int by,int cx,int cy)
{//a到b不经过点c的最短距离 
    if(ax==bx&&ay==by) return 0;//起点就是终点
    memset(vis,0,sizeof(vis)); 
    queue<node>Q;
    while(!Q.empty()) Q.pop();
    node s;s.x=ax,s.y=ay,s.stp=0;
    Q.push(s);
    vis[ax][ay]=1;
    while(!Q.empty())
    {
        node now=Q.front();Q.pop();
        if(now.x==bx&&now.y==by) return now.stp;
        for(int i=0;i<4;i++)
        {
            node nxt;
            nxt.x=now.x+dx[i],nxt.y=now.y+dy[i];
            if(!check(nxt.x,nxt.y)) continue;
            if(nxt.x==cx&&nxt.y==cy) continue;
            if(vis[nxt.x][nxt.y]) continue;
            nxt.stp=now.stp+1;
            Q.push(nxt);
            vis[nxt.x][nxt.y]=1;
        }
    }
    return INF;//不能到达 
}
void Init()
{
    //cnt存状态标号 spfa的时候是把点看成状态 用边来表示状态的转移 
    //[i][j][k]是表示开始的那个点的坐标是(i,j) 空格在它的k方向 
    tot=0;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            for(int k=0;k<4;k++)
                if(mp[i][j]&&check(i+dx[k],j+dy[k]))
                    cnt[i][j][k]=++tot;//给状态标号 没有标号的就是不可行的状态 
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            for(int k=0;k<4;k++)
                if(cnt[i][j][k])
                {
                    int tmp;
                    if(k==0) tmp=1;
                    if(k==1) tmp=0;
                    if(k==2) tmp=3;
                    if(k==3) tmp=2;
                    add(cnt[i][j][k],cnt[i+dx[k]][j+dy[k]][tmp],1);
                    //连单向边就可以了 后面会遍历到旁边状态连回来的
                    //交换空白格子和起点格子 只需要一步  
                }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            for(int k=0;k<4;k++)
                for(int p=0;p<4;p++)//枚举起点格子相邻一圈的两个格子 
                    if(k!=p&&cnt[i][j][k]&&cnt[i][j][p])
                        add(cnt[i][j][k],cnt[i][j][p],bfs(i+dx[k],j+dy[k],i+dx[p],j+dy[p],i,j));
    //可以起点格子不动 而空白格子绕着它转(换方向)
    //如果用起点格子自己换方向 可能没那么优秀因为要绕一绕 
    //而且起点格子有可能会跑到一个离终点的地方去了 更不优 
    //而只动空白格子可能会优秀一些 因为空白格子最优(到处都是活动格子)只用走2~4步 

    //经过其他格子没有任何影响 因为都是1 只是空白格子变了而已
    //而经过起点格子则会改变起点格子的坐标 
}
int dis[N*N];
int spfa()
{
    queue<int>Q;
    while(!Q.empty()) Q.pop();
    if(sx==tx&&sy==ty) return 0;
    memset(dis,INF,sizeof(dis));
    memset(mark,0,sizeof(mark));
    for(int k=0;k<4;k++)
    {//空格先走到起点的四周 初始状态 以空格从起点四周开始 
        if(cnt[sx][sy][k])
        {
            dis[cnt[sx][sy][k]]=bfs(ex,ey,sx+dx[k],sy+dy[k],sx,sy);
            Q.push(cnt[sx][sy][k]);
            mark[cnt[sx][sy][k]]=1; 
        }
    }
    while(!Q.empty())
    {
        int u=Q.front();Q.pop();
        mark[u]=0;
        for(int i=0;i<G[u].size();i++)
        {
            int v=G[u][i].first,w=G[u][i].second;
            if(dis[v]>dis[u]+w)
            {
                dis[v]=dis[u]+w;
                if(!mark[v])
                {
                    Q.push(v);
                    mark[v]=1;
                }
            }
        }
    }
    //空格在哪里不用管 起点格子到了就可以 
    //从空格在起点格子四周随便哪个地方的状态到空格在终点格子四周随便哪个地方的状态
    int res=INF;
    for(int k=0;k<4;k++)
        if(cnt[tx][ty][k])
            res=min(res,dis[cnt[tx][ty][k]]);
    if(res==INF) return -1;//走不到
    return res;     
}
int main() 
{
    n=rd(),m=rd(),q=rd();
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            mp[i][j]=rd();
    Init();
    while(q--)
    {
        ex=rd(),ey=rd(),sx=rd(),sy=rd(),tx=rd(),ty=rd();
        printf("%d\n",spfa());
    }
    return 0;
}