分層圖
分層圖
講真的...感覺有點像那麼一點點的種類並查集
簡單來說,就是把一個圖分成很多層,然後對圖進行一些處理
比較模板一點的東西就是直接在分層圖上跑最短路,這個時候就涉及到了很多決策,每一個決策能進行一些特殊的操作,比如讓某條邊免費(邊權為0,不是把邊切掉),讓某條邊花費減半之類的,這個時候就可以用分層圖了
然後講講分層圖的具體實現。首先就是空間這個東西,你要有k個決策,那麼你就要開k+1倍的空間,每一層空間涉及到一個決策,。然後對於第一層的圖,還是正常的直接建圖,資料怎麼搞你就怎麼搞。對於第二層及以上的圖,將這一層和下一層連一條有向邊,表示你這一次的決策
那麼通過以上的一些分析,對於一個含n個點的圖,我們需要開 n+k * n 的空間,即 n+k * n 個點,舉個例子吧,大概就是以下這張圖(這個圖照搬
具體的實現方法,以例題為例(四倍經驗爽不爽,會把四道題都放在下面的)
這道題非常模板啊(之後那三道題也非常模板),就是對一個非常正常的圖,你需要求一個最短路,但是在你走的時候,你可以對你走的一些邊進行一次改變,把這條邊的邊權改為之前的二分之一,那麼這個時候最樸素的想法就是求一個最短路,並且記錄這條最短路的邊,對這些邊排個序,把最長的幾條邊改了就行了,就有了以下程式(最短路這裡就不講了)
#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
P1948 [USACO08JAN]Telephone Lines S
滾回來繼續更新,開篇放題
一道紫題,還是挺難得,無論是思維難度還是程式設計難度(沒辦法判斷太多了)
首先說明,我的一些思想和程式是參照了此篇題解之後的
首先看到題目的最小費用,以及一些操作,是可以想到搜尋或者是最短路的,但是仔細想一番之後,其實是有點難度的。我們消耗的有兩個東西,一是花費(就是錢),二是油
答案求最小花費,那我們最短路中建圖時,就考慮以每次花費為邊權,兩個端點就是座標就可以了。但是剩下一個油怎麼辦呢,就可以考慮用分層圖的思想
我們分層分的是使用油的狀態:滿油(第1層),消耗了1個單位的油(第2層),消耗了2個單位的油(第3層)······所有油都消耗完了,那麼我們就需要分\(k+1\)層圖
那麼確定如何分層和根據什麼東西分層之後,如何建圖呢?
- 對於每一個點
我們列舉走到當前點的所有使用油的情況,並向下一層連一條花費為0的邊
- 當我們遇到了一個加油站
我們列舉走到這一個加油站的使用油的情況,很明顯只需要列舉第\(2\)層~第\(k+1\)層,然後對當前點建一條指向該加油站邊權為\(a\)的邊,表示你加油這個狀態
再由這個點,向下一層連一條花費為0的邊
- 考慮自己修建加油站的情況
列舉每一種使用油的情況,建一條邊權為\(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;
}