【搜索+最短路優化】NOIP2013-Day2——華容道

前言

這道題我一來就暴力,打着打着卡住了,看了看別人的思路才暴力成功,獲得70分...

大佬們可以暴力八九十分,更巨的大佬們可以正解AC100分...

這讓我一個即將參加CSP-S的小蒟蒻十分慌張qwq...

題目

題目描述

小 B 最近迷上了華容道,可是他總是要花很長的時間才能完成一次。於是,他想到用編程來完成華容道:給定一種局面, 華容道是否根本就無法完成,如果能完成, 最少需要多少時間。

小 B 玩的華容道與經典的華容道遊戲略有不同,遊戲規則是這樣的:

  1. 在一個 n×m 棋盤上有n×m個格子,其中有且只有一個格子是空白的,其餘n×m−1個格子上每個格子上有一個棋子,每個棋子的大小都是 1×1的;

  2. 有些棋子是固定的,有些棋子則是可以移動的;

  3. 任何與空白的格子相鄰(有公共的邊)的格子上的棋子都可以移動到空白格子上。

遊戲的目的是把某個指定位置可以活動的棋子移動到目標位置。

給定一個棋盤,遊戲可以玩 q 次,當然,每次棋盤上固定的格子是不會變的, 但是棋盤上空白的格子的初始位置、 指定的可移動的棋子的初始位置和目標位置卻可能不同。第 i次玩的時候, 空白的格子在第 EXi行第 EYi 列,指定的可移動棋子的初始位置爲第 SXi行第 SYi​列,目標位置爲第 TXi 行第 TYi列。

假設小 B 每秒鐘能進行一次移動棋子的操作,而其他操作的時間都可以忽略不計。請你告訴小 B 每一次遊戲所需要的最少時間,或者告訴他不可能完成遊戲。

輸入格式

第一行有 3個整數,每兩個整數之間用一個空格隔開,依次表示n,m,q;

接下來的 n 行描述一個n×m 的棋盤,每行有m個整數,每兩個整數之間用一個空格隔開,每個整數描述棋盤上一個格子的狀態,0表示該格子上的棋子是固定的,1表示該格子上的棋子可以移動或者該格子是空白的。

接下來的 q 行,每行包含 6 個整數依次是 EXi,EYi,SXi,SYi,TXi,TYi,每兩個整數之間用一個空格隔開,表示每次遊戲空白格子的位置,指定棋子的初始位置和目標位置。

輸出格式

共q行,每行包含 1 個整數,表示每次遊戲所需要的最少時間,如果某次遊戲無法完成目標則輸出−1。

輸入輸出樣例

輸入

3 4 2
0 1 1 1
0 1 1 0
0 1 0 0
3 2 1 2 2 2
1 2 2 2 3 2

輸出

2
-1

說明/提示

【輸入輸出樣例說明】

棋盤上劃叉的格子是固定的,紅色格子是目標位置,圓圈表示棋子,其中綠色圓圈表示目標棋子。

  1. 第一次遊戲,空白格子的初始位置是 (3,2)(圖中空白所示),遊戲的目標是將初始位置在(1,2)上的棋子(圖中綠色圓圈所代表的棋子)移動到目標位置(2,2)(圖中紅色的格子)上。

移動過程如下:

  1. 第二次遊戲,空白格子的初始位置是(1,2)(圖中空白所示),遊戲的目標是將初始位置在(2,2)上的棋子(圖中綠色圓圈所示)移動到目標位置 (3,2)上。

要將指定塊移入目標位置,必須先將空白塊移入目標位置,空白塊要移動到目標位置,必然是從位置(2,2)上與當前圖中目標位置上的棋子交換位置,之後能與空白塊交換位置的只有當前圖中目標位置上的那個棋子,因此目標棋子永遠無法走到它的目標位置, 遊戲無法完成。

【數據範圍】

對於30%的數據,1≤n,m≤10,q=1;

對於 60%的數據,1≤n,m≤30,q≤10;

對於 100%的數據,1≤n,m≤30,q≤500。

題目大意

給定棋盤,上面有可移動和不可移動的棋子,求出使“起點棋子”移至“目標位置”的最小步數

分析

參考博客&特別鳴謝:https://www.cnblogs.com/fengxunling/p/9773648.html

【暴力TLE70~80分】

對於一個棋子,如果它的四周沒有空格的話是移動不了的,只有碰到了空格並與之交換位置纔算移動了

於是只用搜索空格的位置,遇到“起點棋子”就交換位置,“起點棋子”到達目標位置時更新答案即可

Ps.代碼實現時要想清楚...比如“一般的移動”和“與‘起點棋子’交換”的座標變換有區別,這裏調了有一會兒...qwq...


【最短路優化100分】

我們可以發現,之所以會T,原因是我們使用了過多無用的狀態(也就是讓空白格子在圖上瞎跑)。

但是我們需要的狀態其實不多。爲了能讓空白格子推着起始點跑,空白格子是必須在起始點旁邊的。

那麼我們現在就知道什麼是有用狀態了:

即在起始點周圍(即上下左右四個方向)。狀態的轉移呢?

——這個分兩種:

一種是從起始點周圍轉移到周圍的另外一個位置,這個步數可以用一個bfs計算,

另外一種是和起始點交換位置,步數很顯然是1。

接下來的難點是如何記錄狀態。因爲我們把起始點在棋盤中的每個位置和它上下左右的情況都抽離出來了,所以我們可以開一個cnt數組,【cnt[ i ][ j ][ k ]表示起始點在( i , j )的位置,空白格子在它周圍的 k (從0到3編號)位置這個狀態作爲點時點的編號】

之後就是後繼狀態的轉移,我們可以考慮通過連邊的方式,把合法狀態和它的後繼狀態連起來,這樣狀態就有傳遞性和連續性了。

具體我們可以通過下面這張圖來理解:
圖中模擬的是目標節點和空格(假設空格在目標節點的左邊)進行交換(從綠色狀態轉移到黃色狀態)。

按照上面的說法,目標節點和它周圍的狀態是有長度爲1的有向邊的,它的周圍狀態之間也是有bfs出的長度的有向邊的。

我們易知黃色“換後節點”和綠色的“目標節點的左面”的座標其實是一樣的,綠色的“目標節點”和黃色的“換後節點的右面”座標也是一樣的。

但要注意的是它們記錄的狀態不一樣,所以圖還不是連通的。

交換了空格與起始點的位置後,按照上圖就是將綠色的“目標節點的左面”和黃色的“換後節點的右面”連一條有向邊。

連完邊之後spfa跑最短路就可以了...

但是需要注意的一點就是可能開始空白格子和起始點位置離得很大,

因爲我們需要空白格子推着起始點移動,所以我們開始要把空白各自移動到起始點周圍。

(也就是上下左右)這個用bfs求最少步數就可以了。

移動到空白格子周圍就起點到終點連通了,直接最短路跑一遍就可以了。

最後我們把dis[ cnt[ tx ][ ty ][ k ] ]  ( 0<=k<=3 ) 的最小值記爲ans。

暴力70分代碼

//70分暴搜 
#include<cstdio>
#include<cmath>
#include<queue>
#include<vector>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN=50,INF=0x3f3f3f3f;
int dir1[4]={-1,0,1,0},dir2[4]={0,1,0,-1};
bool vis[MAXN+5][MAXN+5][MAXN+5][MAXN+5];
int a[MAXN+5][MAXN+5];
int ex,ey,sx,sy,tx,ty;
int n,m,q,flag,ans;
struct node
{
	int step;//移動步數 
	int x,y;//指定棋子位置 
	int kx,ky;//空格位置 
};
void BFS()
{
	queue<node> que;
	node q;q.x=sx,q.y=sy,q.kx=ex,q.ky=ey,q.step=0;
	vis[ex][ey][sx][sy]=1;
	que.push(q);
	while(!que.empty())
	{
		q=que.front();que.pop();
		if(q.step>=ans)
			continue;		
		if(q.x==tx&&q.y==ty)//到達目標位置 
		{
			flag=true;
			ans=min(ans,q.step);
			continue;
		}
		for(int i=0;i<4;i++)//空格位置 
		{
			int X=q.x,Y=q.y,Kx=q.kx+dir1[i],Ky=q.ky+dir2[i];
			if(!(Kx>=1&&Kx<=n&&Ky>=1&&Ky<=m&&a[Kx][Ky]&&!vis[Kx][Ky][X][Y]))
				continue;
			if(X==tx&&Y==ty)//到達目標位置 
			{
				flag=true;
				ans=min(ans,q.step);
				continue;
			}	
			if(X==Kx&&Y==Ky)//遇到了指定棋子,兩者交換位置 
			{	
				if(vis[Kx][Ky][q.kx][q.ky])
					continue;
				node tmp;tmp.x=q.kx,tmp.y=q.ky,tmp.kx=Kx,tmp.ky=Ky,tmp.step=q.step+1;
				vis[Kx][Ky][q.kx][q.ky]=1;
				que.push(tmp);
			}
			else
			{
				node tmp;tmp.x=X,tmp.y=Y,tmp.kx=Kx,tmp.ky=Ky,tmp.step=q.step+1;
				vis[Kx][Ky][X][Y]=1;
				que.push(tmp);
			}		
		}
	}
}
void init()
{
	flag=0,ans=INF;
	memset(vis,0,sizeof(vis));
}
int main()
{
	scanf("%d%d%d",&n,&m,&q);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			scanf("%d",&a[i][j]);
	while(q--)
	{
		init();
		scanf("%d%d%d%d%d%d",&ex,&ey,&sx,&sy,&tx,&ty);
		if(sx==tx&&sy==ty)
		{
			printf("0\n");
			continue;
		}
		BFS();
		if(flag)
			printf("%d\n",ans);
		else
			printf("-1\n");
	}
	return 0;
}

AC代碼

//最短路優化 
#include<cstdio>
#include<cmath>
#include<queue>
#include<vector>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN=40,MAXM=1e6,INF=0x3f3f3f3f;
int dir1[5]={1,-1,0,0},dir2[5]={0,0,1,-1};//因爲要用"^1"所以下標有點要求(很妙啊!) 
bool vis[MAXN+5][MAXN+5];
int head[MAXM+5],dis[MAXM+5],done[MAXM+5];
int a[MAXN+5][MAXN+5],cnt[MAXN+5][MAXN+5][5];
int n,m,q,ecnt,ans,tot;
struct Edge
{
	int u,v,w,nxt;
}E[MAXM+5];
struct node
{
	int x,y;//位置 
	int step;//移動步數 
};
void Addedge(int u,int v,int w)
{
	E[++ecnt].u=u,E[ecnt].v=v,E[ecnt].w=w;
	E[ecnt].nxt=head[u];
	head[u]=ecnt;
}
bool Check(int x,int y)
{
	if(x>=1&&x<=n&&y>=1&&y<=m&&a[x][y])
		return true;
	return false;
}
int BFS(int ax,int ay,int bx,int by,int cx,int cy)//搜索的起點,終點,不能經過的點 
{
	if(ax==bx&&ay==by)
		return 0;
	memset(vis,0,sizeof(vis));
	vis[ax][ay]=1;
	queue<node> que;
	que.push((node){ax,ay,0});
	while(!que.empty())
	{
		node q=que.front();que.pop();
		if(q.x==bx&&q.y==by)
			return q.step;
		for(int i=0;i<4;i++)
		{
			int dx=q.x+dir1[i],dy=q.y+dir2[i];
			if(Check(dx,dy)&&!vis[dx][dy]&&!(dx==cx&&dy==cy))
			{
				vis[dx][dy]=1;
				que.push((node){dx,dy,q.step+1});
			}
		}
	}
	return INF;
}
int SPFA(int ax,int ay,int bx,int by,int cx,int cy)//最短路求出到其他點(狀態)的步數 
{
	queue<int> que;
	if(bx==cx&&by==cy)
		return 0;
	memset(dis,0x3f,sizeof(dis));
	for(int k=0;k<4;k++)//起點棋子的四周 
		if(cnt[bx][by][k])
		{
			//求出空白格子移到起點棋子四周的步數  
			dis[cnt[bx][by][k]]=BFS(ax,ay,bx+dir1[k],by+dir2[k],bx,by);
			que.push(cnt[bx][by][k]);
			done[cnt[bx][by][k]]=1;
		}
	while(!que.empty())
	{
		int u=que.front();que.pop();done[u]=0;
		for(int i=head[u];i;i=E[i].nxt)
		{
			int v=E[i].v;
			if(dis[v]>dis[u]+E[i].w)
			{
				dis[v]=dis[u]+E[i].w;
				if(!done[v])
				{
					que.push(v);
					done[v]=1;
				}
			}
		}
	}
	for(int k=0;k<4;k++)
		if(cnt[cx][cy][k])
			ans=min(ans,dis[cnt[cx][cy][k]]);
	if(ans==INF)
		return -1;
	return ans;
}
void Init()
{
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			for(int k=0;k<4;k++)
				if(a[i][j]&&a[i+dir1[k]][j+dir2[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])
					Addedge(cnt[i][j][k],cnt[i+dir1[k]][j+dir2[k]][k^1],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])
						Addedge(cnt[i][j][k],cnt[i][j][p],BFS(i+dir1[k],j+dir2[k],i+dir1[p],j+dir2[p],i,j));
}
int main()
{
	scanf("%d%d%d",&n,&m,&q);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			scanf("%d",&a[i][j]);
	Init();
	while(q--)
	{
		ans=INF;
		int ex,ey,sx,sy,tx,ty;
		scanf("%d%d%d%d%d%d",&ex,&ey,&sx,&sy,&tx,&ty);
		printf("%d\n",SPFA(ex,ey,sx,sy,tx,ty));
	}
	return 0;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章