【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 }
View Code

 

当然也有非大众(自称思想过于僵化的神爷)的做法:

如果这道题的图简化成一条链,那么在序列上只需要维护前缀中的最小价格即可,在某个点卖出所赚得的价钱就是 这个点的价格-前缀中的最小价格。

回到朴素的图,要想维护前缀之类的东西,也可以想到按照拓扑序从小到大来更新从起点到每个点的答案……

然而这张图既有有向边又有无向边,因此要想进行拓扑排序,我们需要给他简化成有向图。(划重点)

可以发现,无向边的两个点形成的环可以缩成一个点,那么无向边都被缩掉了,这就变成了一张有向图。在更新从起点到这个点的最大价格时,只有两个点中的最大价格可能有贡献,而在更新从这个点到终点的最小价格时,只有两个点中的最小价格可能有贡献。

然而这个缩点范围还是比较小,因此我们把整张图中的环都分别缩成一个点,那么每个环中只需要存两个可能对答案有贡献的值:所有点的最大价格和最小价格。这样整张图就变成了有向无环图

有向无环图可以做拓扑排序了!

按照拓扑序从小到大更新 从起点到每个点的最小价格 和 到达每个点时卖出水晶球所能赚得的最大价钱。(在有向无环的情况下这样好转移,可自行思考,详细见代码)

这样就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 }
View Code

 

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 }
View Code

 

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]奶牛的旅行

分数规划

posted @ 2018-09-08 16:54  大本营  阅读(365)  评论(0编辑  收藏  举报