分层图初探 By cellur925
因为最近测试遇到了分层图的题目,所以稍微学了一下==。
这种题目一般是来解决最短路边权有变化/有k条免费路的问题的。他们基本都一般有两种实现方式:dp+最短路/分层图+最短路
当然你如果非要说他们是一样的我也没Fa♂反驳qwq
一、dp+最短路(以dij为例)
我们一般的球最短路都是在一维上进行的。设$dis[k]$为从起点到$k$的最短路。但是如果多了条件,比如有$k$条道路可以选择边权变化,那么就可以使用这种方法。
以P4822 [BJWC2012]冻结 这道题为例,它给出的条件是有$k$条路可以把原来的边权变为一半,那么我们就可以设$dis[k][h]$为到达$k$点,已经在$h$条路上把原来的边权变为一半的最短路。这是不是很像dp的状态的呀?是不是呀?
那么在我们平时松弛的时候,就可以看做dp的转移了。这里有两种:使用“改边卡”和不使用“改边卡”。
其他就与普通dij无异了。
当然最后的答案需要在使用0~k张改边卡间取最小值。
#include<cstdio> #include<algorithm> #include<queue> #include<cstring> using namespace std; int n,m,k,tot,ans=2123473647; int head[1090],dis[1090][100],vis[1090][100]; struct node{ int to,next,val; }edge[2090]; struct cellur{ int dis,p,cnt; }; bool operator < (const cellur &a,const cellur &b) { return a.dis>b.dis; } void add(int x,int y,int z) { edge[++tot].to=y; edge[tot].next=head[x]; head[x]=tot; edge[tot].val=z; } void dijkstra() { memset(dis,0x3f,sizeof(dis)); priority_queue<cellur>q; dis[1][0]=0;q.push((cellur){0,1,0}); while(!q.empty()) { int u=q.top().p; int num=q.top().cnt;q.pop(); if(vis[u][num]) continue; vis[u][num]=1; for(int i=head[u];i;i=edge[i].next) { int v=edge[i].to; if(num<k&&dis[v][num+1]>dis[u][num]+edge[i].val/2) { dis[v][num+1]=dis[u][num]+edge[i].val/2; q.push((cellur){dis[v][num+1],v,num+1}); } if(dis[v][num]>dis[u][num]+edge[i].val) { dis[v][num]=dis[u][num]+edge[i].val; q.push((cellur){dis[v][num],v,num}); } } } } int main() { scanf("%d%d%d",&n,&m,&k); for(int i=1;i<=m;i++) { int x=0,y=0,z=0; scanf("%d%d%d",&x,&y,&z); add(x,y,z),add(y,x,z); } dijkstra(); for(int i=0;i<=k;i++) ans=min(ans,dis[n][i]); printf("%d",ans); return 0; }
P4568 [JLOI2011]飞行路线 这是一个同样的题目==。
1 #include<cstdio> 2 #include<algorithm> 3 #include<cstring> 4 #include<queue> 5 6 using namespace std; 7 8 int n,m,k,s,t,tot,ans=2133483647; 9 int head[10090]; 10 int dis[10090][20]; 11 bool vis[10090][20]; 12 struct node{ 13 int to,next,val; 14 }edge[100090]; 15 struct cellur{ 16 int dis,p,cnt; 17 }; 18 bool operator < (const cellur &a,const cellur &b) 19 { 20 return a.dis>b.dis; 21 } 22 23 void add(int x,int y,int z) 24 { 25 edge[++tot].to=y; 26 edge[tot].next=head[x]; 27 head[x]=tot; 28 edge[tot].val=z; 29 } 30 31 void dijkstra() 32 { 33 memset(dis,0x3f,sizeof(dis)); 34 priority_queue<cellur>q; 35 dis[s][0]=0; 36 q.push((cellur){0,s,0}); 37 while(!q.empty()) 38 { 39 int u=q.top().p; 40 int num=q.top().cnt;q.pop(); 41 if(vis[u][num]) continue; 42 vis[u][num]=1; 43 for(int i=head[u];i;i=edge[i].next) 44 { 45 int v=edge[i].to; 46 if(num<k&&dis[v][num+1]>dis[u][num]) 47 { 48 dis[v][num+1]=dis[u][num]; 49 q.push((cellur){dis[v][num+1],v,num+1}); 50 } 51 if(dis[v][num]>dis[u][num]+edge[i].val) 52 { 53 dis[v][num]=dis[u][num]+edge[i].val; 54 q.push((cellur){dis[v][num],v,num}); 55 } 56 } 57 } 58 } 59 60 int main() 61 { 62 scanf("%d%d%d",&n,&m,&k); 63 scanf("%d%d",&s,&t);s++;t++; 64 for(int i=1;i<=m;i++) 65 { 66 int x=0,y=0,z=0; 67 scanf("%d%d%d",&x,&y,&z); 68 x++;y++; 69 add(x,y,z);add(y,x,z); 70 } 71 dijkstra(); 72 for(int i=0;i<=k;i++) 73 ans=min(ans,dis[t][i]); 74 printf("%d",ans); 75 return 0; 76 }
二、分层图最短路
这 是什么?其实给他总结成一句话:就是拆点。
我们知道dp是遍历所有的状态的,那么如果我们在开始建边的时候就考虑将所有的状态都连上边,那么之后跑裸的最短路就行了。
如何建边?我们以P2939 [USACO09FEB]改造路Revamping Trails 这道题为例。(其实这三道题基本一样,只是这道题用拆点方法写了)
题目也是要求我们有$k$条边可以把它的边权变为0。那么我们可以把每个点拆成$k+1$个点,从0到$k$,第$i$个表示到这里要用$i$次改边卡。
可以参考这段代码感性理解下。
for(int i=1;i<=m;i++) { int x=0,y=0,z=0; scanf("%d%d%d",&x,&y,&z); for(int j=0;j<=k;j++) { add(x+j*n,y+j*n,z);add(y+j*n,x+j*n,z);//同层之间正常连边 if(j!=k) add(x+j*n,y+(j+1)*n,0),add(y+j*n,x+(j+1)*n,0);//边权有变化了 } }
之后跑一遍裸的dij后,我们同理在终点的所有可能状态中遍历取最小值。
for(int i=0;i<=k;i++) ans=min(ans,dis[n+i*n]);
个人认为这种方法的缺点是很难控制空间的开销。因为点数增加了$k+1$倍,边数也增加了很多倍。容易出现RE/MLE的情况。
#include<cstdio> #include<algorithm> #include<cstring> #include<queue> using namespace std; int n,m,k,tot,ans=1000000000; int head[210090],dis[210090],vis[210090]; struct node{ int to,next,val; }edge[4200090]; void add(int x,int y,int z) { edge[++tot].to=y; edge[tot].next=head[x]; head[x]=tot; edge[tot].val=z; } void dijkstra() { priority_queue<pair<int,int> >q; memset(dis,0x3f,sizeof(dis)); dis[1]=0;q.push(make_pair(0,1)); while(!q.empty()) { int u=q.top().second;q.pop(); if(vis[u]) continue; vis[u]=1; for(int i=head[u];i;i=edge[i].next) { int v=edge[i].to; if(dis[v]>dis[u]+edge[i].val) { dis[v]=dis[u]+edge[i].val; q.push(make_pair(-dis[v],v)); } } } } int main() { scanf("%d%d%d",&n,&m,&k); for(int i=1;i<=m;i++) { int x=0,y=0,z=0; scanf("%d%d%d",&x,&y,&z); for(int j=0;j<=k;j++) { add(x+j*n,y+j*n,z);add(y+j*n,x+j*n,z); if(j!=k) add(x+j*n,y+(j+1)*n,0),add(y+j*n,x+(j+1)*n,0); } } dijkstra(); for(int i=0;i<=k;i++) ans=min(ans,dis[n+i*n]); printf("%d",ans); return 0; }
Over?
我们看一道不是那么套路的题吧!
给你一个无向图,求从1到n经过的边的边权绝对值之和最小的路径。而每经过一条边,这条边的边权就会改变。原边权为x,那么新边权就会变成1/1-x。
关于1/1-x这个式子,其实他是很有规律的,在进行三次迭代之后,它会回到原值。
举个栗子。设x=2,此函数为$f(x)$。那么
$f(1)=-1$
$f(-1)=1/2$
$f(1/2)=2$。是不是很神奇鸭?
那么我们可以仿照之前的方法,拆点。把一个点拆成三种状态,每个状态就是到它的那条边是它本身的第几种边权。
for(int i=1;i<=n;i++) for(int j=0;j<=2;j++) id[i][j]=++cnt; for(int i=1;i<=m;i++) { int x=0,y=0; double z=0; scanf("%d%d%lf",&x,&y,&z); add(id[x][0],id[y][1],z);add(id[y][0],id[x][1],z); z=1.0/(1-z);double tmp=fabs(z); add(id[x][1],id[y][2],tmp);add(id[y][1],id[x][2],tmp); z=1.0/(1-z);tmp=fabs(z); add(id[x][2],id[y][0],tmp);add(id[y][2],id[x][0],tmp); }
然后再跑dij就行惹。
#include<cstdio> #include<algorithm> #include<queue> #include<cstring> #include<cmath> #define maxn 100090 #define maxm 300090 using namespace std; const int inf=0x3f3f3f3f; int n,m,tot,cnt; int id[maxn][4],head[maxn*3]; bool vis[maxn*3]; double ans,dis[maxn*3]; struct node{ int to,next; double val; }edge[maxm*6]; void add(int x,int y,double z) { edge[++tot].to=y; edge[tot].next=head[x]; head[x]=tot; edge[tot].val=z; } void dijkstra() { priority_queue<pair<double,int> >q; for(int i=1;i<=cnt;i++) dis[i]=inf; q.push(make_pair(0,id[1][0])); dis[id[1][0]]=0; while(!q.empty()) { int x=q.top().second;q.pop(); if(vis[x]) continue; vis[x]=1; for(int i=head[x];i;i=edge[i].next) { int y=edge[i].to; if(dis[y]>dis[x]+edge[i].val) { dis[y]=dis[x]+edge[i].val; q.push(make_pair(-dis[y],y)); } } } } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) for(int j=0;j<=2;j++) id[i][j]=++cnt; for(int i=1;i<=m;i++) { int x=0,y=0; double z=0; scanf("%d%d%lf",&x,&y,&z); add(id[x][0],id[y][1],z);add(id[y][0],id[x][1],z); z=1.0/(1-z);double tmp=fabs(z); add(id[x][1],id[y][2],tmp);add(id[y][1],id[x][2],tmp); z=1.0/(1-z);tmp=fabs(z); add(id[x][2],id[y][0],tmp);add(id[y][2],id[x][0],tmp); } dijkstra(); ans=min(dis[id[n][0]],min(dis[id[n][1]],dis[id[n][2]])); printf("%.3lf\n",ans); return 0; }
感觉这种题还是比较套路的,只要发现是分层图,建边或跑dp都不难想的qwq。还有更多拓展的题目,给自己再留下一个天坑。
(光速逃)