P1606 [USACO07FEB]白銀蓮花池 &&P1979 華容道(bfs預處理+狀態連邊建圖+最短路算法求解,最短路計數)

這幾天寫題是真的自閉,連圖都不會建了

P1606 [USACO07FEB]白銀蓮花池

P1979 華容道

題目大意

不可描述,自己看去.

解題思路

bfsbfs預處理+狀態連邊建圖+最短路算法求解

這是這類題一個非常重要的思想.

通過bfsbfs建圖將各個狀態連接,然後用最短路算法求出最優的狀態.

bfsbfs預處理+狀態連邊建圖部分:P1606 [USACO07FEB]白銀蓮花池

一開始我的想法很簡單,就是每個點都進行bfsbfs,處理出每個點能夠到達的點.

			if(j-2>=1&&i-1>=1){
				if(mp[i-1][j-2]!=2&&!check[num[i][j]][num[i-1][j-2]]){
					check[num[i][j]][num[i-1][j-2]]=true;
					check[num[i-1][j-2]][num[i][j]]=true;
					if(mp[i-1][j-2]==0||mp[i][j]==0)add(num[i][j],num[i-1][j-2],1);
					else add(num[i][j],num[i-1][j-2],0);
				}
			}//其中的一部分代碼

但是這種方法不可行. 爲什麼呢?因爲這樣會有0邊權邊的出現. 這對於我們統計最短路數是非常不利的.

而且我們會發現一個問題:什麼情況下的邊應該賦值爲1,什麼情況應該賦值爲0呢?這也是不清楚的.

在這裏插入圖片描述

如圖所示,如果按照如上代碼的寫法,會出現明明只需要2的值但卻算出來3的情況,這就需要我們從同一個方向(這樣才能真正反映需要填上荷葉的水的數量)統計.比如說我們如果只在指向點爲0的時候總邊權+1,那麼答案就沒有錯

我們考慮上面那張圖,可以發現,我們可以每個0節點爲起點,預處理出花費1個荷葉能到達的點,這樣就保證了邊權全都爲1.

同樣地,由於每次我們都只是從當前點往前一個點,所以上述“同一個方向統計”的要求也滿足. 只不過現在因爲以每個0節點爲起點,所以我們的“花費一個荷葉”指的就是用一個荷葉把我們0腳下的水填了.

不難發現,如此一來我們就要用bfsbfs找到八個方向每個方向第一個不能不加荷葉跳過去的點(也就是0點)了.

bool tocheck(int x,int y){
	if(x<1||y<1||x>n||y>m||mp[x][y]==2||vis[x][y])return false;//注意如果是mp=2即一塊石頭,跳過去是沒有意義的
	return true;
}
void build_map(int x,int y,int u){
	for(int i=0;i<8;i++){
		int a=x+drc[i][0],b=y+drc[i][1];
		if(tocheck(a,b)){
			vis[a][b]=true;//防止重複走
			if(mp[a][b]==1)build_map(a,b,u);//如果當前還能不加荷葉繼續走,那麼就繼續往前走
			else add(u,num[a][b]);//如果是終點mp=4的話,那麼就連一條邊過去好了;否則說明只加一個荷葉最多就只能走到mp=0的這個位置,要繼續往前走,就要加荷葉了
		}
	}
	return ;
}

相當於我們只計起點的0變成1,所以就保證了要填荷葉的水和每點間的邊權等

bfsbfs預處理+狀態連邊建圖部分:P1979 華容道

考驗狀態記錄技巧的一道好題!

因爲我們只要考慮指定塊的位置,而指定塊位置的移動和空白塊有關,所以只需記錄指定塊和空白塊的位置. 我們可以記錄(x1,y1,x2,y2)(x1,y1,x2,y2)表示指定塊在(x1,y1)(x1,y1),空白塊在(x2,y2)(x2,y2)的狀態. 由於空白塊可以四方向移動,所以每個狀態會向四個狀態連邊. 這樣共有(nm)2(nm)^2個狀態,總複雜度爲O(q(nm)2)O(q(nm)^2),只能通過60%的數據.

但是我們可以發現:只有空白塊位於指定塊的四方向上,指定塊纔可以移動. 所以,我們可以記(x1,y1,dir)(x1,y1,dir)表示指定塊在(x1,y1)(x1,y1),空白塊在指定塊的dirdir方向的狀態。這樣狀態只有4nm4nm個.(程序中是用kmn+numk*m*n+num實現的,num爲指定塊的編號)

接下來我們考慮各個狀態之間的連邊. 首先,空白塊和指定塊可以交換位置,這兩個狀態連邊的邊權爲1;

其次,假定空白塊在指定塊上方,空白塊可以通過若干步移動來到空白塊下/左/右方。這些狀態連邊的邊權我們可以通過bfsbfs計算出來.

這樣就構造出了一張圖,先把空白塊移動到目標塊旁邊,之後向目標狀態(空白塊可以位於指定塊的四個方向)做最短路即可.

最短路算法:P1606 [USACO07FEB]白銀蓮花池

由於我們需要在求最短路的同時求最短路的數目,那麼這就是一個最短路計數問題了.

解題關鍵是,每一個點的最短路徑數是由連接它的前一個點決定的.

在邊權爲1的情況下spfaspfa才成立,否則老老實實用dpdp

若還沒學過最短路計數:P1144 最短路計數

bool check[1010];
int dis[1010];
ll sum[1010];
void spfa(int st){
	memset(dis,inf,sizeof dis);
	memset(check,false ,sizeof check);
	queue<int >q;
	q.push(st);
	check[st]=true;
	dis[st]=0;
	sum[st]=1;
	while(!q.empty()){
		int u=q.front();
		for(int i=head[u];i;i=e[i].next ){
			int v=e[i].v ;
			if(dis[v]>dis[u]+1){
				dis[v]=dis[u]+1;
				sum[v]=sum[u];//修改當前最短路數量,此時前面的記錄都得推翻重做
				if(!check[v]){
					check[v]=true;
					q.push(v);
				}
			}
			else if(dis[v]==dis[u]+1)sum[v]+=sum[u];//根據加法原理,最短路數量增加
		}
		check[u]=false;
		q.pop();
	}
}

最短路算法:P1979華容道

spfaspfa複雜度爲O(qknm)O(qknm),可以通過100%的數據.

這東西還有什麼好說的嗎?

其他

編號方法:如圖

1 2 3
4 5 6
7 8 9
10 11 12
num[i][j]=(i-1)*m+j;

程序實現

P1606 [USACO07FEB]白銀蓮花池

#include<bits/stdc++.h>
#define ll long long
#define inf 0x3f3f3f3f
using namespace std;
struct edge{
	int v,next;
}e[100010];//考慮30*30*k,k爲可以直接到達的點,可能不止8個(一個可以外拓的子節點又可以有8個),所以數組儘量開大
int drc[8][2]={{-2,1},{2,-1},{-1,2},{1,-2},{-2,-1},{-1,-2},{2,1},{1,2}};//預設八個方向
int head[1010],tot;
int tx,ty,sx,sy;
int n,m,ans1;
ll ans2;
void add(int u,int v){
	e[++tot].v =v;
	e[tot].next =head[u];
	head[u]=tot;
}
int mp[51][51],num[51][51];
bool vis[51][51];
bool tocheck(int x,int y){
	if(x<1||y<1||x>n||y>m||mp[x][y]==2||vis[x][y])return false;
	return true;
}
void build_map(int x,int y,int u){
	for(int i=0;i<8;i++){
		int a=x+drc[i][0],b=y+drc[i][1];
		if(tocheck(a,b)){
			vis[a][b]=true;
			if(mp[a][b]==1)build_map(a,b,u);
			else add(u,num[a][b]);
		}
	}
	return ;
}//bfs+連邊操作
void prepare(){
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			if(!mp[i][j]||mp[i][j]==3){//找空節點或起點,起點多算的1在spfa後減回來就行了
				memset(vis,false ,sizeof vis);
				vis[i][j]=true;
				build_map(i,j,num[i][j]);
			}
		}
	}
}
bool check[1010];
int dis[1010];
ll sum[1010];//不開long long見祖先
void spfa(int st){
	memset(dis,inf,sizeof dis);
	memset(check,false ,sizeof check);
	queue<int >q;
	q.push(st);
	check[st]=true;
	dis[st]=0;
	sum[st]=1;
	while(!q.empty()){
		int u=q.front();
		for(int i=head[u];i;i=e[i].next ){
			int v=e[i].v ;
			if(dis[v]>dis[u]+1){
				dis[v]=dis[u]+1;
				sum[v]=sum[u];
				if(!check[v]){
					check[v]=true;
					q.push(v);
				}
			}
			else if(dis[v]==dis[u]+1)sum[v]+=sum[u];
		}
		check[u]=false;
		q.pop();
	}
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			scanf("%d",&mp[i][j]);
			num[i][j]=(i-1)*m+j;
			if(mp[i][j]==3)sx=i,sy=j;
			if(mp[i][j]==4)tx=i,ty=j;
		}
	}
	prepare();
	spfa(num[sx][sy]);
	ans1=dis[num[tx][ty]];
	ans2=sum[num[tx][ty]];
	if(ans1==inf)printf("-1\n");
	else printf("%d\n%lld\n",ans1-1,ans2);//記得把起點的邊多算的1減回來
	return 0;
}

P1979 華容道

//規定空格子在指定格子上方(i+1)的狀態爲num+cnt,下方(i-1)爲num+cnt*2,左方(j-1)爲num+cnt*3,右方(j+1)爲num+cnt*4
#include<bits/stdc++.h>
#define maxn 10010
#define inf 0x3f3f3f3f
using namespace std;
struct edge{
	int v,w,next;
}e[maxn];
int n,m,q,minn,cnt;
int ex,ey,sx,sy,tx,ty;
int num[101][101],mp[101][101];
int tot,head[maxn];
void add(int u,int v,int w){
	e[++tot].v =v;
	e[tot].w =w;
	e[tot].next =head[u];
	head[u]=tot;
	e[++tot].v =u;
	e[tot].w =w;
	e[tot].next =head[v];
	head[v]=tot;
}//雙向建邊,即兩個狀態可同步數相互轉化
int sum[101][101];//表示到這個格子的步數
bool vis[101][101];
int bfs(int u,int dirc,int sta){//bfs求當前空格在編號爲u位置,移動到dirc位置,不能經過sta(指定格子)位置所需最小步數
	memset(vis,false,sizeof vis);
	memset(sum,0,sizeof sum);
	int stx=((sta%m)?(sta/m+1):(sta/m)),sty=((sta%m)?(sta%m):m);//轉化回二維形式,注意取模什麼的
	vis[stx][sty]=true;
	int x=((u%m)?(u/m+1):(u/m)),y=((u%m)?(u%m):m);
	sum[x][y]=0;
	vis[x][y]=true;
	queue<int >q;
	q.push(u);
	while(!q.empty()){
		int w=q.front();
		q.pop();
		int a=((w%m)?(w/m+1):(w/m)),b=((w%m)?(w%m):m);
		if(mp[a-1][b]&&!vis[a-1][b]){
			vis[a-1][b]=true;
			sum[a-1][b]=sum[a][b]+1;
			int ww=num[a-1][b];
			if(ww==dirc){return sum[a-1][b];break;}
			q.push(ww);
		}
		if(mp[a+1][b]&&!vis[a+1][b]){
			vis[a+1][b]=true;
			sum[a+1][b]=sum[a][b]+1;
			int ww=num[a+1][b];
			if(ww==dirc){return sum[a+1][b];break;}
			q.push(ww);
		}
		if(mp[a][b+1]&&!vis[a][b+1]){
			vis[a][b+1]=true;
			sum[a][b+1]=sum[a][b]+1;
			int ww=num[a][b+1];
			if(ww==dirc){return sum[a][b+1];break;}
			q.push(ww);
		}
		if(mp[a][b-1]&&!vis[a][b-1]){
			vis[a][b-1]=true;
			sum[a][b-1]=sum[a][b]+1;
			int ww=num[a][b-1];
			if(ww==dirc){return sum[a][b-1];break;}
			q.push(ww);
		}//以上爲向外拓展步數1步
	}
	return 0;//false!! 如果到不了,返回的是0,所以輸入中空白格子已經在旁邊的情況要特判
}
void prepare(){
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			if(!mp[i][j])continue;
			if(mp[i+1][j]&&mp[i-1][j]){
				int k=bfs(num[i+1][j],num[i-1][j],num[i][j]);
				if(k)add(num[i][j]+cnt,num[i][j]+cnt*2,k);
			}
			if(mp[i+1][j]&&mp[i][j+1]){
				int k=bfs(num[i+1][j],num[i][j+1],num[i][j]);
				if(k)add(num[i][j]+cnt,num[i][j]+cnt*4,k);
			}
			if(mp[i+1][j]&&mp[i][j-1]){
				int k=bfs(num[i+1][j],num[i][j-1],num[i][j]);
				if(k)add(num[i][j]+cnt,num[i][j]+cnt*3,k);
			}
			if(mp[i-1][j]&&mp[i][j+1]){
				int k=bfs(num[i-1][j],num[i][j+1],num[i][j]);
				if(k)add(num[i][j]+cnt*2,num[i][j]+cnt*4,k);
			}
			if(mp[i-1][j]&&mp[i][j-1]){
				int k=bfs(num[i-1][j],num[i][j-1],num[i][j]);
				if(k)add(num[i][j]+cnt*2,num[i][j]+cnt*3,k);
			}
			if(mp[i][j+1]&&mp[i][j-1]){
				int k=bfs(num[i][j+1],num[i][j-1],num[i][j]);
				if(k)add(num[i][j]+cnt*3,num[i][j]+cnt*4,k);
			}
			if(mp[i+1][j])add(num[i][j]+cnt,num[i+1][j]+cnt*2,1);
			//if(mp[i-1][j])add(num[i][j]+cnt*2,num[i-1][j]+cnt,1);
			if(mp[i][j-1])add(num[i][j]+cnt*3,num[i][j-1]+cnt*4,1);
			//if(mp[i][j+1])add(num[i][j]+cnt*4,num[i][j+1]+cnt*3,1);
		}
	}
}//prepare預處理每個點四周(上下左右)互相通達所需的步數

//規定空格子在指定格子上方(i+1)的狀態爲num+cnt,下方(i-1)爲num+cnt*2,左方(j-1)爲num+cnt*3,右方(j+1)爲num+cnt*4
int dis[maxn];
bool check[maxn];
void spfa(int u){
	memset(dis,inf,sizeof dis);
	memset(check,false,sizeof check);
	dis[u]=0;
	check[u]=true;
	queue<int >q;
	q.push(u);
	while(!q.empty()){
		int now=q.front();
		for(int i=head[now];i;i=e[i].next ){
			int v=e[i].v ;
			if(dis[v]>dis[now]+e[i].w ){
				dis[v]=dis[now]+e[i].w ;
				if(!check[v]){
					check[v]=true;
					q.push(v);
				}
			}
		}
		q.pop();
		check[now]=false;
	}
}//對狀態求最短路
int main(){
	scanf("%d%d%d",&n,&m,&q);
	cnt=n*m;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			scanf("%d",&mp[i][j]);
			num[i][j]=(i-1)*m+j;//編號
		}
	}
	prepare();
	for(int i=1;i<=q;i++){
	//ex,ey,空白格子;sx,sy,指定格子;tx,ty,目標格子
		scanf("%d%d%d%d%d%d",&ex,&ey,&sx,&sy,&tx,&ty);
		if(sx==tx&&sy==ty){printf("0\n");continue;}//特判1,不用移動,重合
		int k1=inf,k2=inf,k3=inf,k4=inf;
		int w=num[ex][ey];
		minn=inf;
		 
		if(ex==sx+1&&ey==sy){
			minn=inf;
			spfa(num[sx][sy]+cnt);//由於已經在旁邊,所以直接求最短路即可
			for(int j=1;j<=4;j++){
				minn=min(minn,dis[num[tx][ty]+j*cnt]);//從目標格子的四個方向的距離中選擇最小的
			}
			if(minn>=inf)printf("-1\n");
			else printf("%d\n",minn);
			continue;
		}
		else if(ex==sx-1&&ey==sy){
			minn=inf;
			spfa(num[sx][sy]+2*cnt);
			for(int j=1;j<=4;j++){
				minn=min(minn,dis[num[tx][ty]+j*cnt]);
			}
			if(minn>=inf)printf("-1\n");
			else printf("%d\n",minn);
			continue;		
		}
		else if(ey==sy+1&&ex==sx){
			minn=inf;
			spfa(num[sx][sy]+4*cnt);
			for(int j=1;j<=4;j++){
				minn=min(minn,dis[num[tx][ty]+j*cnt]);
			}
			if(minn>=inf)printf("-1\n");
			else printf("%d\n",minn);
			continue;	
		}
		else if(ey==sy-1&&ex==sx){
			minn=inf;
			spfa(num[sx][sy]+3*cnt);
			for(int j=1;j<=4;j++){
				minn=min(minn,dis[num[tx][ty]+j*cnt]);
			}
			if(minn>=inf)printf("-1\n");
			else printf("%d\n",minn);
			continue;
		}//以上爲特判2,不用把空白格子移動到當前格子旁邊,已經在旁邊了
		
//規定空格子在指定格子上方(i+1)的狀態爲num+cnt,下方(i-1)爲num+cnt*2,左方(j-1)爲num+cnt*3,右方(j+1)爲num+cnt*4
//同理k1,k2,k3,k4表示的方向
		if(mp[sx+1][sy]&&bfs(w,num[sx+1][sy],num[sx][sy])){
			minn=inf;
			k1=bfs(w,num[sx+1][sy],num[sx][sy]);//k表示先把空白格子移動到指定格子旁邊所需的步數,由於有4個方向,所以寫了4個if
			spfa(num[sx][sy]+cnt);
			for(int j=1;j<=4;j++){
				minn=min(minn,dis[num[tx][ty]+j*cnt]);
			}//這是從目標格子的四個方向的最短距離中取最小的
			k1+=minn;
		}
		if(mp[sx-1][sy]&&bfs(w,num[sx-1][sy],num[sx][sy])){
			minn=inf;
			k2=bfs(w,num[sx-1][sy],num[sx][sy]);
			spfa(num[sx][sy]+cnt*2);
			for(int j=1;j<=4;j++){
				minn=min(minn,dis[num[tx][ty]+j*cnt]);
			}
			k2+=minn;
		}
		if(mp[sx][sy-1]&&bfs(w,num[sx][sy-1],num[sx][sy])){
			minn=inf;
			k3=bfs(w,num[sx][sy-1],num[sx][sy]);
			spfa(num[sx][sy]+cnt*3);
			for(int j=1;j<=4;j++){
				minn=min(minn,dis[num[tx][ty]+j*cnt]);
			}
			k3+=minn;
		}
		if(mp[sx][sy+1]&&bfs(w,num[sx][sy+1],num[sx][sy])){
			minn=inf;
			k4=bfs(w,num[sx][sy+1],num[sx][sy]);
			spfa(num[sx][sy]+cnt*4);
			for(int j=1;j<=4;j++){
				minn=min(minn,dis[num[tx][ty]+j*cnt]);
			}
			k4+=minn;
		}//以上爲一般情況
		minn=min(k1,k2);
		minn=min(k3,minn);
		minn=min(k4,minn);//求最小值
		if(minn>=inf){printf("-1\n");continue;}//有加法,所以是大於等於
		printf("%d\n",minn);
	}
	return 0;
}

題後總結

建圖真的很重要!以後看到這一類型的題,就想一下可不可以建圖抽象化地表示狀態,然後通過最短路算法表示狀態的改變,從而求解.

可以通過條件推知要建什麼樣的圖,建的圖沒有一定的要求,入鄉隨俗就好. 比如說雙向圖啊,單向圖啊,都是不一定的.

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