【2018.9.8】最短路专题
T1:Cqoi2009 最优贸易
题意就是让你在从1到n的路径上把商品以最低价格买入后以最高价格卖出。
大众解法就是把图正反各建一遍,然后从起点和终点分别跑一次spfa,计算 从起点到每个点的最小值 和 从每个点到终点的最大值。最后的答案就是每个点上存的最大值减最小值 的最大值。
1 #include<bits/stdc++.h> 2 #define N 100001 3 #define M 500001 4 using namespace std; 5 inline int read(){ 6 int x=0; bool f=1; char c=getchar(); 7 for(;!isdigit(c);c=getchar()) if(c=='-') f=0; 8 for(; isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+c-'0'; 9 if(f) return x; 10 return 0-x; 11 } 12 int n,m,cost[N]; 13 14 struct edge{ 15 int v,next; 16 }e[M<<1],re[M<<1]; 17 int head[N],cnt; 18 inline void add(int u,int v){ 19 e[++cnt].v=v, e[cnt].next=head[u], head[u]=cnt; 20 } 21 22 int rev_head[N],rev_cnt; 23 inline void rev_add(int u,int v){ 24 re[++rev_cnt].v=v, re[rev_cnt].next=rev_head[u], rev_head[u]=rev_cnt; 25 } 26 27 int dis[2][N]; 28 bool vis[N],inque[N]; 29 30 void spfa(){ 31 queue<int>Q; 32 dis[0][1]=cost[1]; 33 Q.push(1); 34 35 int u,i; 36 while(!Q.empty()){ 37 u=Q.front(), Q.pop(), inque[u]=0; 38 for(i=head[u];i;i=e[i].next){ 39 if(!vis[e[i].v]){ 40 vis[e[i].v]=1; 41 dis[0][e[i].v]=cost[e[i].v]; 42 if(!inque[e[i].v]) {Q.push(e[i].v); inque[e[i].v]=1;} 43 } 44 if(dis[0][e[i].v]>dis[0][u]){ 45 dis[0][e[i].v]=dis[0][u]; 46 if(!inque[e[i].v]) {Q.push(e[i].v); inque[e[i].v]=1;} 47 } 48 } 49 } 50 } 51 52 void rev_spfa(){ 53 queue<int>Q; 54 memset(vis,0,sizeof vis); 55 dis[1][n]=cost[n]; 56 Q.push(n); 57 58 int u,i; 59 while(!Q.empty()){ 60 u=Q.front(), Q.pop(), inque[u]=0; 61 for(i=rev_head[u];i;i=re[i].next){ 62 if(!vis[re[i].v]){ 63 vis[re[i].v]=1; 64 dis[1][re[i].v]=cost[re[i].v]; 65 if(!inque[re[i].v]) {Q.push(re[i].v); inque[re[i].v]=1;} 66 } 67 if(dis[1][re[i].v]<dis[1][u]){ 68 dis[1][re[i].v]=dis[1][u]; 69 if(!inque[re[i].v]) {Q.push(re[i].v); inque[re[i].v]=1;} 70 } 71 } 72 } 73 } 74 75 int main(){ 76 n=read(),m=read(); 77 int i,x,y,z; 78 for(i=1;i<=n;i++) cost[i]=read(); 79 for(i=1;i<=m;i++){ 80 x=read(),y=read(),z=read(); 81 add(x,y); 82 if(z==2) add(y,x); 83 rev_add(y,x); 84 if(z==2) rev_add(x,y); 85 } 86 spfa(); 87 rev_spfa(); 88 int ans=0; 89 for(i=1;i<=n;i++) ans = max(ans, dis[1][i]-dis[0][i]); 90 printf("%d\n", ans>0 ? ans : 0); 91 return 0; 92 }
当然也有非大众(自称思想过于僵化的神爷)的做法:
如果这道题的图简化成一条链,那么在序列上只需要维护前缀中的最小价格即可,在某个点卖出所赚得的价钱就是 这个点的价格-前缀中的最小价格。
回到朴素的图,要想维护前缀之类的东西,也可以想到按照拓扑序从小到大来更新从起点到每个点的答案……
然而这张图既有有向边又有无向边,因此要想进行拓扑排序,我们需要给他简化成有向图。(划重点)
可以发现,无向边的两个点形成的环可以缩成一个点,那么无向边都被缩掉了,这就变成了一张有向图。在更新从起点到这个点的最大价格时,只有两个点中的最大价格可能有贡献,而在更新从这个点到终点的最小价格时,只有两个点中的最小价格可能有贡献。
然而这个缩点范围还是比较小,因此我们把整张图中的环都分别缩成一个点,那么每个环中只需要存两个可能对答案有贡献的值:所有点的最大价格和最小价格。这样整张图就变成了有向无环图。
有向无环图可以做拓扑排序了!
按照拓扑序从小到大更新 从起点到每个点的最小价格 和 到达每个点时卖出水晶球所能赚得的最大价钱。(在有向无环的情况下这样好转移,可自行思考,详细见代码)
这样就ok了么?你有没有注意到起点是1?
这意味着什么?拓扑排序时 缩点前的1号点所在的缩后点 不能被其他点更新,必须最先入队。而你缩点后,因为原来其它点有可能被其它连通分量的点指向,所以缩点后有可能有其它点指向 缩点前的1号点 所在的缩后点,因此要将它的入度设置为0!
1 #include<algorithm> 2 #include<iostream> 3 #include<cstring> 4 #include<cstdio> 5 #include<cmath> 6 #define LL long long 7 #define M 500020 8 #define N 100020 9 using namespace std; 10 int read(){ 11 int nm=0,fh=1; char cw=getchar(); 12 for(;!isdigit(cw);cw=getchar()) if(cw=='-') fh=-fh; 13 for(;isdigit(cw);cw=getchar()) nm=nm*10+(cw-'0'); 14 return nm*fh; 15 } 16 int be[N],fs[N],nt[M<<1],to[M<<1],dfn[N],low[N],cnt,ind[N],F[N],G[N]; 17 int tot,u,v,n,m,mi[N],mx[N],p[N],tmp,S[N],top,st[M],ed[M],ans; 18 int q[N],hd,tl; 19 void link(int x,int y){nt[tmp]=fs[x],fs[x]=tmp,to[tmp++]=y;} 20 void fd(int x){ 21 dfn[x]=low[x]=++cnt,S[++top]=x; 22 for(int i=fs[x];i!=-1;i=nt[i]){ 23 if(dfn[to[i]]==0) fd(to[i]); 24 if(be[to[i]]==0) low[x]=min(low[x],low[to[i]]); 25 } 26 if(dfn[x]>low[x]) return; 27 for(mi[++tot]=M;S[top+1]!=x;top--){ 28 be[S[top]]=tot; 29 mi[tot]=min(mi[tot],p[S[top]]); 30 mx[tot]=max(mx[tot],p[S[top]]); 31 } 32 G[tot]=mi[tot]; 33 } 34 void torp(){ 35 while(hd<tl){ 36 int x=q[hd++]; 37 for(int i=fs[x];i!=-1;i=nt[i]){ 38 if(--ind[to[i]]==0&&to[i]!=be[1]) q[tl++]=to[i]; 39 } 40 } 41 } 42 void DP(){ 43 hd=tl=0,q[tl++]=be[1]; 44 while(hd<tl){ 45 int x=q[hd++]; 46 for(int i=fs[x];i!=-1;i=nt[i]){ 47 G[to[i]]=min(G[to[i]],G[x]); 48 F[to[i]]=max(max(F[x],mx[to[i]]-G[to[i]]),F[to[i]]); 49 if(--ind[to[i]]==0) q[tl++]=to[i]; 50 } 51 } 52 } 53 int main(){ 54 n=read(),m=read(),memset(fs,-1,sizeof(fs)); 55 for(int i=1;i<=n;i++) p[i]=read(); 56 for(int i=1;i<=m;i++){ 57 u=read(),v=read(),link(u,v),st[i]=u,ed[i]=v; 58 if(read()>1) link(v,u); 59 } 60 for(int i=1;i<=n;i++) if(!dfn[i]) fd(i); 61 memset(fs,-1,sizeof(fs)),tmp=top=0; 62 for(int i=1;i<=m;i++) if(be[st[i]]!=be[ed[i]]) link(be[st[i]],be[ed[i]]),ind[be[ed[i]]]++; 63 for(int i=1;i<=tot;i++) if(ind[i]==0&&i!=be[1]) q[tl++]=i; 64 torp(),DP(),printf("%d\n",F[be[n]]); return 0; 65 }
T2:汽车加油行驶
裸的最短路题目,考建图。
观察到k出奇的小,考虑根据还能行驶的长度(即油量)k分层。
考虑加油站:
对于有加油站的格子(题目要求必须加油),向满油量的那一层的对应格连一条权值为$a$的边。
对于没有加油站的格子,向满油量的那一层的对应格连一条权值为$a+c$的边。
当油量不为0时,
对于任意格子,向当前油量-1的那一层的上下左右四格各连一条权值为$0$的边。
建完图后跑最短路即可(建议dijkstra)起点为 $<1,1,k>$, 终点为 $<n,n,x>$, $0≤x≤k$。
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 #include<queue> 5 using namespace std; 6 7 const int N = 2e5 + 50; 8 const int M = 1e6; 9 const int inf = 0x3f3f3f3f; 10 11 int NXT[M], TO[M], V[M]; 12 int DIS[N], VIS[N], HD[N]; 13 int MAP[205][205]; 14 int C[205][205][15]; 15 int ss, tt, sz, k, n, m, a, b, c, tot; 16 queue<int> q; 17 18 int mk(int x, int y, int z) { 19 return z * n * n + (x - 1) * n + y; 20 } 21 22 void add(int x, int y, int z) { 23 TO[sz] = y; V[sz] = z; 24 NXT[sz] = HD[x]; HD[x] = sz++; 25 } 26 27 int spfa() { 28 memset(DIS, 0x3f, sizeof (DIS)); 29 DIS[ss] = 0; 30 q.push(ss); 31 while (!q.empty()) { 32 int u = q.front(); 33 VIS[u] = 0; 34 q.pop(); 35 for (int i = HD[u]; i != -1; i = NXT[i]) { 36 int v = TO[i]; 37 if (DIS[v] > DIS[u] + V[i]) { 38 DIS[v] = DIS[u] + V[i]; 39 if (!VIS[v]) { 40 VIS[v] = 1; 41 q.push(v); 42 } 43 } 44 } 45 } 46 return DIS[tt]; 47 } 48 49 int main() { 50 memset(HD, -1, sizeof (HD)); 51 scanf("%d%d%d%d%d", &n, &k, &a, &b, &c); 52 for (int i = 1; i <= n; ++i) 53 for (int j = 1; j <= n; ++j) 54 scanf("%d", &MAP[i][j]); 55 tt = 200010; 56 for (int x = 1; x <= n; ++x) { 57 for (int y = 1; y <= n; ++y) { 58 for (int i = k; i >= 0; --i) { 59 tot = mk(x, y, i); 60 if (x == 1 && y == 1 && i == k) 61 ss = tot; 62 if (x == n && y == n) 63 add(tot, tt, 0); 64 if (i != k) 65 if (MAP[x][y]) add(tot, mk(x, y, k), a); 66 else add(tot, mk(x, y, k), a + c); 67 if (i != 0) { 68 if (MAP[x][y] && i != k) continue; //注意避开这种情况 69 if (x != 1) 70 add(tot, mk(x - 1, y, i - 1), b); 71 if (y != 1) 72 add(tot, mk(x, y - 1, i - 1), b); 73 if (x != n) 74 add(tot, mk(x + 1, y, i - 1), 0); 75 if (y != n) 76 add(tot, mk(x, y + 1, i - 1), 0); 77 } 78 } 79 } 80 } 81 printf("%d\n", spfa()); 82 return 0; 83 }
T4:Word rings
提到“相同的字符相连”,我们就能想到连边建图咯。
我们可以把单词看成有向边,把两个字符串首尾可连接的相同字母 看成点的方法。例如,对单词ababc就是点"ab"向点"bc"连一条长度为5的边。
问题就转化成在图中找一个环,使得环上边权的平均值最大。
由于建出来的图是有向图,直接搜索所有环的时间复杂度平均为 O(N*M*玄学)(n为点数,m为边数),可能超时。(无向图可以用记忆化搜索的方式 O(n) 找环)
那该怎么快速找平均权值最大的环呢?
请先百度一下01分数规划的思想。这是某神爷的01分规笔记(原文)(有改进):
做了spfa求平均值最小的环的问题,刚意识到“原来这是01分数规划啊”,就在这里并不对劲地说分数规划问题了。
01分数规划解决的是哪一种问题呢?有两个大小一样的数组A[1...n]和B[1...n],要求出数组$Q[1...k]$,使$(A[Q[1]] + A[Q[2]] + ... + A[Q[3]]) / ( B[Q[1]] + B[Q[2]] + ... + B[Q[3]])$最大。
有一种听上去很靠谱的贪心做法:把所有位置i按照A[i]/B[i]排序,取最靠前的k个。
这样如果$A[1...n]$和$B[1...n]$全是正数就没什么问题,但是要是有的是负数呢?想必是会出错的,因为如果对于位置$i$,$A[i]$正$B[i]$负,$(A[i]/B[i])$就是个负数,会排在靠后的位置,但是$B[i]$为负,会使分母减少,如果分母减$B[i]$为正的话反而该选$B[i]$。要是考虑了这种情况,又要考虑选了太多负数导致最后算出的分数是负数的情况、分母和分子都是负数使得答案负负得正的情况…听上去很麻烦,不可做。
但是会发现当存在
$x≤( A[Q[1]] + ... + A[Q[k]]) / ( B[Q[1]] + ... + B[Q[k]])$
时,对于$x'<x$一定存在
$x'<=( A[Q'[1]] + ... + A[Q'[k]]) / ( B[Q'[1]] + ... + B[Q'[k]])$
,有单调性的话,就可以二分了。
那该怎么判断是否存在$Q[1...k]$使得
$x ≤ ( A[Q[1]] + ... + A[Q[k]]) / ( B[Q[1]] + ... + B[Q[k]])$
呢?稍微变一下形。
$x * ( B[Q[1]] + ... + B[Q[k]]) ≤ ( A[Q[1]] + ... + A[Q[k]]) $
$0 ≤ ( A[Q[1]] + ... + A[Q[k]]) - x * ( B[Q[1]] + ... + B[Q[k]])$
$0 ≤ ( A[Q[1]] - x * B[Q[1]]) + ... + (A[Q[k]] - x * B[Q[k]])$
这时每一个下标的影响就统一了,可以用贪心思想:对于所有位置$i$,将所有$A[Q[i]]-x*B[Q[i]]$从大到小排序,选出前k大的值相加,判断其是否 $\geq 0$ 即可。
分数规划问题想必是不可能这么简单的,可能还会有其他奇怪的限制(不是只限制个数k),或者是每个数可以取多个。据说分数规划题的难度不在分数规划上?
有了上述思想,就不难发现这题的解法了:
由于$Average=(E1+E2+...+Ek)/K$,
所以$Average*K=E1+E2+...+Ek$,
即$(E1-Average)+(E2-Average)+...+(Ek-Average) \geq 0$
于是根据01分数规划思想,二分出最大的满足条件的平均数$Average$,check时把所有边权减去这个平均数,原本的找图中平均权值最大的环就变成了判断图中是否有正环。找任意一个正环直接用dfs版的spfa即可,从每个点开始做dfs,速度比bfs快。但是只用dfs还不够高效,还需要在判定前把起点到每个点的初始距离的最小值设为0(也就是说如果在当前找到的链的权值和为负数时就不继续往下深搜。因为“从每个点”开始做dfs意味着如果一个环是正环,那么这个环一定有一个遍历顺序,能使得遍历过程中经过的边权的和总是$>0$。这个结论目前我也不会证,但经过不断发现是这样的)
搞坏spfa题目:hdu4889(这题让你造数据卡spfa)(网格图,横向边权巨大,纵向边权巨小,然后随便卡)
强大的最短路题目:bzoj2612 bzoj4283
存:
SPFA的两种优化SLF和LLL
BZOJ 4898: [Apio2017]商旅 题解
bzoj5110: [CodePlus2017]Yazid 的新生舞会
[Usaco2007 Dec]奶牛的旅行
分数规划