【题解】P1979 [NOIP2013 提高组] 华容道(SPFA,BFS,常数优化)

【题解】P1979 [NOIP2013 提高组] 华容道

最近打比赛次次挂。。平均每周得被至少一场比赛打击一次(这周好不容易 ABC 打的还行模拟赛又挂……)心烦意乱。写篇题解疏散一下内心的苦闷(雾)。。。

这题正解是 SPFA+BFS,但是我直接 BFS 搜过了……

我才不会告诉你我提交了五十多遍卡常从 70 分卡 AC 了……

这是一篇玄学卡常题解(雾)。


题目链接

P1979 [NOIP2013 提高组] 华容道 - 洛谷

题意概述

有一个 \(n \times m\) 的棋盘,棋盘上的每个格子要么是空格要么是棋子,满足以下三种情况:

  1. 每个棋盘只有一个空格;

  2. 有些棋子可移动,有些棋子不可移动。

  3. 每个空格任何与空白的格子相邻(有公共的边)的格子上的棋子都可以移动到空白格子上。

题目要求把某个指定位置可以活动的棋子移动到目标位置。

给定空格格子的坐标,所有不可移动棋子(方便起见以下均简称为“障碍”)的坐标,以及需要移动的棋子的初始坐标和目标位置坐标,求最少需要几步;无解输出 -1

题目分析

以下均为我当时这道题完整的思考过程。

第一眼看到这道题,我的第一直觉是 BFS。将空格作为起点,每次分别与相邻可交换的格子交换,并将每次交换后的结果插入队列中,直到需要移动的棋子移动到目标位置为止。

要注意的一点是,我们在一般的题目中的 BFS,判断一个状态是否走过采用的是利用一个二维数组 \(vis_{x,y}\) 标记,但这里由于不仅要表示空格在哪,还要表示要移动的棋子现在移动到了哪,所以我们用 \(vis_{x,y,nx,ny}\) 表示棋子现在移动到了 \((x,y)\) 并且当前空格在 \((nx,ny)\) 这个状态是否访问过。然后就可以愉快的 BFS 了。

于是就有了下面的代码(相信不用我多说):

const int maxn=35;
int a[maxn][maxn],dis[maxn],vis[maxn][maxn][maxn][maxn];
int dx[4]={0,0,1,-1};
int dy[4]={1,-1,0,0};
int ex,ey,sx,sy,tx,ty;
int n,m,Q;
bool f;

struct Node{int x,y,xx,yy,t;};//t 是一个时间戳,用来表示当前状态下的最小时间。

void BFS()
{
    if(sx==tx&&sy==ty){cout<<0<<endl;f=true;return ;}
    //若起点等于终点,直接特判即可。
    queue<Node>q;
    q.push(Node{sx,sy,ex,ey,0});
    memset(vis,0,sizeof(vis));//由于有 q 组询问,请不要忘记初始化。
    vis[sx][sy][ex][ey]=1;
    while(!q.empty())
    {
        Node now=q.front();
        q.pop();
        for(int i=0;i<4;i++)
        {
            int x=now.x,y=now.y;
            int nx=now.xx+dx[i];
            int ny=now.yy+dy[i];//更新空格的横纵坐标。
            if(x==nx&&y==ny){x=now.xx;y=now.yy;}//若棋子与空格交换位置,则需更新棋子当前位置。
            if(nx<=0||nx>n||ny<=0||ny>m)continue;//边界判断
            if(vis[x][y][nx][ny]||a[nx][ny]==0||a[x][y]==0)continue;//显然如果当前节点是障碍或者已经访问过则 continue。
            vis[x][y][nx][ny]=1;
            Node tmp=Node{x,y,nx,ny,now.t+1};
            q.push(tmp);
            if(x==tx&&y==ty)
            {
                cout<<now.t+1<<endl;f=true;return ;//若棋子移动到目标位置则直接输出时间并返回。
            }
        }
    }
}

int main()
{
    n=read();m=read();Q=read();
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            a[i][j]=read();
        }
    }
    while(Q--)
    {
        ex=read();ey=read();sx=read();sy=read();tx=read();ty=read();
        f=false;
        BFS();
        if(f==false)cout<<-1<<endl;//无解。
    }
    return 0;
}

单次询问时间复杂度为 \(O((nm)^2)\)\(q\) 次询问时间复杂度 \(O(q(nm)^2)\)

至此,我们就获得了 70pts 的好成绩!

接下来,我们介绍一些常见的卡常技巧:

  1. 开 O2。相信这个不用我多说;

  2. 使用快读快写,这里有封装好的模板:

    inline int read()
    {
        int x=0,f=1;char ch=getchar();
        while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
        while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
        return x*f;
    }
    
    inline void write(int x)
    {
        if(x<0){putchar('-');x=-x;}
        if(x>9)write(x/10);
        putchar(x%10+'0');
    }
  3. 对于换行符,不要用 cout<<endl;,直接 putchar('\n');

用了这些之后,我们就已经获得了 85pts 的好成绩,如图:

这里说明一下,其实当时我刚开始本来就想骗点暴力分然后打正解的。然后走到这一步,发现 T 了的三个点都在可观测范围之内,心里想:诶,我的程序卡一卡说不定有救……

然后就走上了一条卡常了 2 天然后 AC 的不归路(不是)。

走到这里之后的很长一段时间我此题的分数一直停留在 85pts 不变,正当我要放弃的时候,去讨论区瞅了一眼发现可以采用循环展开的方法,便有了如下代码(这里只提供了 BFS 部分,其它地方与上述代码相同):

void BFS()
{
    if(sx==tx&&sy==ty){cout<<0<<endl;f=true;return ;}
    queue<Node>q;
    q.push(Node{sx,sy,ex,ey,0});
    memset(vis,0,sizeof(vis));
    vis[sx][sy][ex][ey]=1;
    while(!q.empty())
    {
        Node now=q.front();
        q.pop();
        int x,y,nx,ny;
        int i=0;
        x=now.x;y=now.y;
        nx=now.xx+dx[i];
        ny=now.yy+dy[i];
        if(x==nx&&y==ny){x=now.xx;y=now.yy;}
        if(nx>=1&&nx<=n&&ny>=1&&ny<=m&&!vis[x][y][nx][ny]&&a[nx][ny]&&a[x][y])
        {
            vis[x][y][nx][ny]=1;
            Node tmp=Node{x,y,nx,ny,now.t+1};
            q.push(tmp);
            if(x==tx&&y==ty)
            {
                write(now.t+1);putchar('\n');f=true;return ;
            }
        }
        i++;
        x=now.x;y=now.y;
        nx=now.xx+dx[i];
        ny=now.yy+dy[i];
        if(x==nx&&y==ny){x=now.xx;y=now.yy;}
        if(nx>=1&&nx<=n&&ny>=1&&ny<=m&&!vis[x][y][nx][ny]&&a[nx][ny]&&a[x][y])
        {
            vis[x][y][nx][ny]=1;
            Node tmp=Node{x,y,nx,ny,now.t+1};
            q.push(tmp);
            if(x==tx&&y==ty)
            {
                write(now.t+1);putchar('\n');f=true;return ;
            }
        }
        i++;
        x=now.x;y=now.y;
        nx=now.xx+dx[i];
        ny=now.yy+dy[i];
        if(x==nx&&y==ny){x=now.xx;y=now.yy;}
        if(nx>=1&&nx<=n&&ny>=1&&ny<=m&&!vis[x][y][nx][ny]&&a[nx][ny]&&a[x][y])
        {
            vis[x][y][nx][ny]=1;
            Node tmp=Node{x,y,nx,ny,now.t+1};
            q.push(tmp);
            if(x==tx&&y==ty)
            {
                write(now.t+1);putchar('\n');f=true;return ;
            }
        }
        i++;
        x=now.x;y=now.y;
        nx=now.xx+dx[i];
        ny=now.yy+dy[i];
        if(x==nx&&y==ny){x=now.xx;y=now.yy;}
        if(nx>=1&&nx<=n&&ny>=1&&ny<=m&&!vis[x][y][nx][ny]&&a[nx][ny]&&a[x][y])
        {
            vis[x][y][nx][ny]=1;
            Node tmp=Node{x,y,nx,ny,now.t+1};
            q.push(tmp);
            if(x==tx&&y==ty)
            {
                write(now.t+1);putchar('\n');f=true;return ;
            }
        }
    }
}

据说这样做相当于优化了 \(10^8\) 级别的常数。

于是我成功地拿到了 90pts:

可以发现,到现在为止,我们已经把两个的 TLE 都卡到了 0.05s 以内。

然后我们可以对于 i++ 这一步操作做一些改变,进行一点优化:

int x,y,nx,ny;
//int i=0;
x=now.x;y=now.y;
nx=now.xx;ny=now.yy-1;
if(x==nx&&y==ny){x=now.xx;y=now.yy;}
if(nx>=1&&nx<=n&&ny>=1&&ny<=m&&!vis[x][y][nx][ny]&&a[nx][ny]&&a[x][y])
{
    vis[x][y][nx][ny]=1;
    Node tmp=Node{x,y,nx,ny,now.t+1};
    q.push(tmp);
    if(x==tx&&y==ty)
    {
        write(now.t+1);putchar('\n');f=true;return ;
    }
}
x=now.x;y=now.y;
nx=now.xx;ny=now.yy+1;
if(x==nx&&y==ny){x=now.xx;y=now.yy;}
if(nx>=1&&nx<=n&&ny>=1&&ny<=m&&!vis[x][y][nx][ny]&&a[nx][ny]&&a[x][y])
{
    vis[x][y][nx][ny]=1;
    Node tmp=Node{x,y,nx,ny,now.t+1};
    q.push(tmp);
    if(x==tx&&y==ty)
    {
        write(now.t+1);putchar('\n');f=true;return ;
    }
}
//i++;
x=now.x;y=now.y;
nx=now.xx+1;ny=now.yy;
if(x==nx&&y==ny){x=now.xx;y=now.yy;}
if(nx>=1&&nx<=n&&ny>=1&&ny<=m&&!vis[x][y][nx][ny]&&a[nx][ny]&&a[x][y])
{
    vis[x][y][nx][ny]=1;
    Node tmp=Node{x,y,nx,ny,now.t+1};
    q.push(tmp);
    if(x==tx&&y==ty)
    {
        write(now.t+1);putchar('\n');f=true;return ;
    }
}
//i++;
x=now.x;y=now.y;
nx=now.xx-1;ny=now.yy;
if(x==nx&&y==ny){x=now.xx;y=now.yy;}
if(nx>=1&&nx<=n&&ny>=1&&ny<=m&&!vis[x][y][nx][ny]&&a[nx][ny]&&a[x][y])
{
    vis[x][y][nx][ny]=1;
    Node tmp=Node{x,y,nx,ny,now.t+1};
    q.push(tmp);
    if(x==tx&&y==ty)
    {
        write(now.t+1);putchar('\n');f=true;return ;
    }
}

省去了变量 \(i\) 以及 \(i++\) 的操作。于是:

……………………

…………

……

第十一个点 1s…………。

想起来一句歌词:

卡常卡不过,优化这,优化那,却还多那一两秒~ ——《WC 2019 OI 之梦》

然后我又思考了好久好久,优化了一些小的内容,但是都没能卡进这一秒的时限……

后来,当我几乎要放弃的时候,盯着程序看了好久,突然发现了一个东西:

对于当前状态是否可行(即是否可以入队)判断时,我们有一句代码:

if(nx>=1&&nx<=n&&ny>=1&&ny<=m&&!vis[x][y][nx][ny]&&a[nx][ny]&&a[x][y])

前四个是判断边界,第三个是判断之前是否走过该状态,最后两个分别判断空格和棋子是否走到一个障碍点。

但实际上!我们并不需要最后一个判断!因为棋子所在位置一定不可能是障碍物,因为在整个过程中棋子只有可能在原初始坐标或者与空格交换,但是这两种情况下棋子所在都不可能障碍位置!

于是,我怀着激动兴奋的心情删去了最后那一句判断,于是:

AC 了!!!!!!!!!!!!!!!!!!!!!

第十一个点以 977ms 的优异成绩卡进了时限!

我不得不说,十年前的题,放在现在快到飞起的评测机下,搜索 AC 也完全不是不可能。

总结

  1. 在一些题目正解想不到或者写起来很难写的情况下,不妨考虑搜索剪枝,虽然有时候说这是投机取巧,但不排除这种方式反而有可能在赛场上为你得到高分。

  2. 一些常用的卡常技巧:

    • 快读快写;

    • O2 优化;

    • 循环展开;

    • 换行符用 putchar('\n');

    • 对于类似 \(i++\) 类语句可以想办法减少或者不用;

    • 变量名可以定义在循环外面(虽然说我并没有在思路分析中说,但实际上我的 AC 代码中可以体现出来,下见 AC 代码);

    • 去掉一些不必要的判断条件。

代码实现

//luoguP3716
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<string>
#include<algorithm>
using namespace std;
const int maxn=34;
int a[maxn][maxn],dis[maxn];
bool vis[maxn][maxn][maxn][maxn];
//int dx[4]={0,0,1,-1};
//int dy[4]={-1,1,0,0};
int ex,ey,sx,sy,tx,ty;
int n,m,Q;
bool f;

struct Node{int x,y,xx,yy,t;};

inline int read()
{
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
	return x*f;
}

inline void write(int x)
{
    if(x<0){
    	putchar('-');
		x=-x;
	}
    if(x>9)
		write(x/10);
    putchar(x%10+'0');
}

void BFS()
{
	if(sx==tx&&sy==ty){cout<<0<<'\n';f=true;return ;}
	queue<Node>q;
	q.push(Node{sx,sy,ex,ey,0});
	memset(vis,0,sizeof(vis));
	vis[sx][sy][ex][ey]=1;
    int x,y,nx,ny;//变量定义在循环外部。
	while(!q.empty())
	{
		Node now=q.front();
		q.pop();
		//int i=0;
		x=now.x;y=now.y;
		nx=now.xx;ny=now.yy-1;
		if(x==nx&&y==ny){x=now.xx;y=now.yy;}
		if(nx>=1&&nx<=n&&ny>=1&&ny<=m&&!vis[x][y][nx][ny]&&a[nx][ny])
		{
			vis[x][y][nx][ny]=1;
			Node tmp=Node{x,y,nx,ny,now.t+1};
			q.push(tmp);
			if(x==tx&&y==ty)
			{
				write(now.t+1);putchar('\n');f=true;return ;
			}
	    }
		x=now.x;y=now.y;
		nx=now.xx;ny=now.yy+1;
		if(x==nx&&y==ny){x=now.xx;y=now.yy;}
		if(nx>=1&&nx<=n&&ny>=1&&ny<=m&&!vis[x][y][nx][ny]&&a[nx][ny])
		{
			vis[x][y][nx][ny]=1;
			Node tmp=Node{x,y,nx,ny,now.t+1};
			q.push(tmp);
			if(x==tx&&y==ty)
			{
				write(now.t+1);putchar('\n');f=true;return ;
			}
		}
		//i++;
		x=now.x;y=now.y;
		nx=now.xx+1;ny=now.yy;
		if(x==nx&&y==ny){x=now.xx;y=now.yy;}
		if(nx>=1&&nx<=n&&ny>=1&&ny<=m&&!vis[x][y][nx][ny]&&a[nx][ny])
		{
			vis[x][y][nx][ny]=1;
			Node tmp=Node{x,y,nx,ny,now.t+1};
			q.push(tmp);
			if(x==tx&&y==ty)
			{
				write(now.t+1);putchar('\n');f=true;return ;
			}
		}
		//i++;
		x=now.x;y=now.y;
		nx=now.xx-1;ny=now.yy;
		if(x==nx&&y==ny){x=now.xx;y=now.yy;}
		if(nx>=1&&nx<=n&&ny>=1&&ny<=m&&!vis[x][y][nx][ny]&&a[nx][ny])
		{
			vis[x][y][nx][ny]=1;
			Node tmp=Node{x,y,nx,ny,now.t+1};
			q.push(tmp);
			if(x==tx&&y==ty)
			{
				write(now.t+1);putchar('\n');f=true;return ;
			}
		}
	}
}

int main()
{
    //ios::sync_with_stdio(false);
	n=read();m=read();Q=read();
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			a[i][j]=read();
		}
	}
	while(Q--)
	{
		ex=read();ey=read();sx=read();sy=read();tx=read();ty=read();
		f=false;
		BFS();
		if(f==false){write(-1);putchar('\n');}
	}
	return 0;
}

后记

这道题我从 5.27 下午调到 5.28 下午,提交了 50 多遍,创下了我提交次数最多的题目的记录。只有我自己知道有多艰辛。

我的提交记录:

但 OI 就是这样,虽然 OI 知识艰涩难懂,虽然 OI 题目难入登天,但每个 OIer 都怀着那份对 OI 最纯粹的热爱,义无反顾地走在这条孤独的道路上,未曾说过放弃。

我也是如此,入坑 OI 两年了,虽然还是很菜,虽然还是一次次的被吊打,一次次的经历挫折,但是问我是否后悔,我还是会坚定地说出:

此生无悔入 OI,来世还做信竞人。

热爱和信仰。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值