zoukankan      html  css  js  c++  java
  • 【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
    ", 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
    ",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
    ", 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]奶牛的旅行

    分数规划

  • 相关阅读:
    NPOI开发手记
    jQuery.form开发手记
    jQuery.Flot开发手记
    node.js初探
    Linux私房菜阅读笔记
    c#实现常用排序算法
    Excel自定义函数开发手记
    浅谈知识管理
    Git学习手记
    c# 屏蔽快捷键
  • 原文地址:https://www.cnblogs.com/scx2015noip-as-php/p/2018_9_8.html
Copyright © 2011-2022 走看看