1. 程式人生 > 實用技巧 >分層圖

分層圖

分層圖

講真的...感覺有點像那麼一點點的種類並查集

簡單來說,就是把一個圖分成很多層,然後對圖進行一些處理

比較模板一點的東西就是直接在分層圖上跑最短路,這個時候就涉及到了很多決策,每一個決策能進行一些特殊的操作,比如讓某條邊免費(邊權為0,不是把邊切掉),讓某條邊花費減半之類的,這個時候就可以用分層圖了

然後講講分層圖的具體實現。首先就是空間這個東西,你要有k個決策,那麼你就要開k+1倍的空間,每一層空間涉及到一個決策,。然後對於第一層的圖,還是正常的直接建圖,資料怎麼搞你就怎麼搞。對於第二層及以上的圖,將這一層和下一層連一條有向邊,表示你這一次的決策

那麼通過以上的一些分析,對於一個含n個點的圖,我們需要開 n+k * n 的空間,即 n+k * n 個點,舉個例子吧,大概就是以下這張圖(這個圖照搬

我同桌的

具體的實現方法,以例題為例(四倍經驗爽不爽,會把四道題都放在下面的)

P4822 凍結

這道題非常模板啊(之後那三道題也非常模板),就是對一個非常正常的圖,你需要求一個最短路,但是在你走的時候,你可以對你走的一些邊進行一次改變,把這條邊的邊權改為之前的二分之一,那麼這個時候最樸素的想法就是求一個最短路,並且記錄這條最短路的邊,對這些邊排個序,把最長的幾條邊改了就行了,就有了以下程式(最短路這裡就不講了)

#include <bits/stdc++.h>
using namespace std;
int n,m,k,u,v,t,tot,ans;
int dis[20010],vis[20010],head[20010];
priority_queue<int> qq; 
priority_queue<pair<int,int> > q;

struct node {
	int to,net,val;
} e[20010];

struct nodes {
	int id,num;
} pre[20010];

inline void add(int u,int v,int w) {
	e[++tot].to=v;
	e[tot].val=w;
	e[tot].net=head[u];
	head[u]=tot;
}

inline void dijkstra() {
	memset(dis,20050206,sizeof(dis));
	memset(vis,0,sizeof(vis));
	dis[1]=0;
	q.push(make_pair(0,1));
	while(!q.empty()) {
		int x=q.top().second;
		q.pop();
		if(vis[x]) continue;
		vis[x]=1;
		for(register int i=head[x];i;i=e[i].net) {
			int v=e[i].to;
			if(dis[v]>dis[x]+e[i].val) {
				dis[v]=dis[x]+e[i].val;
				pre[v].id=x;
				pre[v].num=e[i].val;
				q.push(make_pair(-dis[v],v));
			}
		}
	}
}

int main() {
	scanf("%d%d%d",&n,&m,&k);
	for(register int i=1;i<=m;i++) {
		scanf("%d%d%d",&u,&v,&t);
		add(u,v,t);
		add(v,u,t);
	}
	dijkstra();
	int kk=n;
	while(kk!=1) {
		qq.push(pre[kk].num);
		kk=pre[kk].id;
	}
	while(k--) {
		if(qq.empty()) break;
		ans+=qq.top()/2;
		qq.pop();
	}
	if(qq.empty()) {
		printf("%d",ans);
		return 0;
	}
	while(!qq.empty()) {
		ans+=qq.top();
		qq.pop();
	}
	printf("%d",ans);
	return 0;
}

那麼如果你是這樣寫的,恭喜你有70分啦,你甚至會發現你連第一個點都是WA,那麼舉個例子為什麼你會錯呢?

以下這個圖,你跑出來的最短路應該是
1 -> 4 -> 5 -> 3 決策一次之後為18
然而另一條路應該是
1 -> 2 -> 3 決策一次之後為17
那這樣你就錯遼

那麼這樣的話,就應該用分層圖來做,那麼直接看程式碼

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e4+50;
int n,m,k;
int head[MAXN*50],tot;
struct node{
	int net,to,w;
}e[MAXN*50];
int d[MAXN*50];
bool v[MAXN*50];  //注意要多開k倍 
void add(int u,int v,int w){
	e[++tot].to=v;
	e[tot].net=head[u];
	e[tot].w=w;
	head[u]=tot;
} //非常正常地村邊 
priority_queue< pair<int,int> > q;
void dij(int s){
	fill(d,d+MAXN*50,20040915); //memset只能賦初值為0或-1,其他值應該是fill,不怎麼了解的最好就用memset 
	memset(d,0x3f,sizeof d);
	memset(v,false,sizeof v);
	d[s]=0;
	q.push(make_pair(0,s));
	while(!q.empty()){
		int x=q.top().second;
		q.pop();
		if(v[x]==true) continue;
		v[x]=true;
		for(register int i=head[x];i;i=e[i].net){
			int y=e[i].to,z=e[i].w;
			if(d[y]>d[x]+z){
				d[y]=d[x]+z;
				q.push(make_pair(-d[y],y));
			}
		}
	}
} //非常正常的最短路 
int main(){
	scanf("%d%d%d",&n,&m,&k);
	for(register int i=1;i<=m;i++){
		int a,b,t;
		scanf("%d%d%d",&a,&b,&t);
		add(a,b,t);
		add(b,a,t); //雙向變 
		for(register int j=1;j<=k;j++){
			add(a+j*n,b+j*n,t);
			add(b+j*n,a+j*n,t); //在每一層中建一個正常地邊 
			add(a+(j-1)*n,b+j*n,t/2); 
			add(b+(j-1)*n,a+j*n,t/2); //在下一層連一條單向邊,表示你的決策,邊權視題目而定 
		}  
	}
	for(register int i=1;i<=k;i++) add(n+(i-1)*n,n+i*n,0); //終點單獨連邊 
	dij(1); //跑最短路 
	cout<<d[n+k*n]; //k個決策,所有第k+1層才是最後的答案 
	return 0;
}

那麼這道題你就成功地A掉了啊,那麼接下來就是非常快樂的三倍經驗時刻,我會把題目掛在下面(題目不完全相同,但是隻是略微區別),然後對於分層圖還有另外一種做法,對於空間要求來說更低,蒟蒻暫且不會,之後應該會更吧

P2939 [USACO09FEB]Revamping Trails G

P4568 [JLOI2011]飛行路線

P1948 [USACO08JAN]Telephone Lines S

然後就是 大佬的部落格蒟蒻自己的部落格


滾回來繼續更新,開篇放題

P4009 汽車加油行駛問題

一道紫題,還是挺難得,無論是思維難度還是程式設計難度(沒辦法判斷太多了)

首先說明,我的一些思想和程式是參照了此篇題解之後的

首先看到題目的最小費用,以及一些操作,是可以想到搜尋或者是最短路的,但是仔細想一番之後,其實是有點難度的。我們消耗的有兩個東西,一是花費(就是錢),二是油

答案求最小花費,那我們最短路中建圖時,就考慮以每次花費為邊權,兩個端點就是座標就可以了。但是剩下一個油怎麼辦呢,就可以考慮用分層圖的思想

我們分層分的是使用油的狀態:滿油(第1層),消耗了1個單位的油(第2層),消耗了2個單位的油(第3層)······所有油都消耗完了,那麼我們就需要分\(k+1\)層圖

那麼確定如何分層和根據什麼東西分層之後,如何建圖呢?

  1. 對於每一個點

我們列舉走到當前點的所有使用油的情況,並向下一層連一條花費為0的邊

  1. 當我們遇到了一個加油站

我們列舉走到這一個加油站的使用油的情況,很明顯只需要列舉第\(2\)層~第\(k+1\)層,然後對當前點建一條指向該加油站邊權為\(a\)的邊,表示你加油這個狀態

再由這個點,向下一層連一條花費為0的邊

  1. 考慮自己修建加油站的情況

列舉每一種使用油的情況,建一條邊權為\(a+c\)的邊

然後直接跑最短路就可以了,但是因為這道題還要涉及到一些細節的問題,我直接放在程式裡面講

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+51;
int n,k,a,b,c;
int hh; //這是一個輔助變數 
struct node{
	int to,net,w;
}e[MAXN]; //正常的連邊 
int head[MAXN],tot;
void add(int x,int y,int z,int xx,int yy,int zz,int w){
	int s=hh*(z-1)+(x-1)*n+y;
	int t=hh*(zz-1)+(xx-1)*n+yy;
	//這個地方和之前把二維轉化為一維不太一樣
	//因為涉及到使用油的情況,有點像一個三維的圖
	//之前的公式是 (x-1)*n+y
	//這裡應該是 z*n*n+(x-1)*n+y
	e[++tot].w=w;
	e[tot].to=t;
	e[tot].net=head[s];
	head[s]=tot;
}
int oil;
int d[MAXN];
bool v[MAXN];
queue<int>q;
void spfa(int s){ //正常的SPFA 
	for(register int i=1;i<=hh*(k+1);i++){ //看上面的公式,這裡應該有n*n*(k+1)個點 
		d[i]=220040915;
		v[i]=false;
	}
	d[s]=0;
	v[s]=true;
	q.push(s);
	while(!q.empty()){
		int x=q.front();
		q.pop();
		v[x]=false;
		for(register int i=head[x];i;i=e[i].net){
			int y=e[i].to,z=e[i].w;
			if(d[y]>d[x]+z){
				d[y]=d[x]+z;
				if(v[y]==false){
					v[y]=true;
					q.push(y);
				}
			}
		}
	}
}
int main(){
	scanf("%d%d%d%d%d",&n,&k,&a,&b,&c);
	hh=n*n; //懶得寫n*n了,多寫個hh 
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j){
			scanf("%d",&oil);
			for(int l=1;l<=k;++l) add(i,j,l,i,j,l+1,0); //向下一層建一條邊
			if(oil){ //如果是加油站 
				for(int l=2;l<=k+1;++l) add(i,j,l,i,j,1,a); //強行給你把油加滿,花費a塊錢 
				//向四個方向建邊
				//因為已經加滿油了,所以固定的建第一層到第二層的邊就可以了 
				if(i<n) add(i,j,1,i+1,j,2,0); 
				if(j<n) add(i,j,1,i,j+1,2,0);
				//注意往上和往左走是要給錢的 
				if(i>1) add(i,j,1,i-1,j,2,b);
				if(j>1) add(i,j,1,i,j-1,2,b);
			}else{
				//列舉每一種用油的情況 
				for(int l=1;l<=k;++l){ //不迴圈到k+1,你不可能油空了還能用吧 
				    //同上 
					if(i<n) add(i,j,l,i+1,j,l+1,0);
					if(j<n) add(i,j,l,i,j+1,l+1,0);
					if(i>1) add(i,j,l,i-1,j,l+1,b);
					if(j>1) add(i,j,l,i,j-1,l+1,b);
				}
				for(int l=2;l<=k+1;++l) add(i,j,l,i,j,1,a+c);
				//建一個加油站,不從1開始,是因為直接加滿了 
			}
		}
	}
	spfa(1);
	int ans=220040915;
	for(int i=1;i<=k+1;++i) ans=min(ans,d[hh*i]); //列舉到最後的終點時,每一種用油的情況,花費最下的一種 
	cout<<ans;
	return 0;
}