1. 程式人生 > 實用技巧 >[LOJ3255][JOI 2020 Final]奧運公交(最短路)

[LOJ3255][JOI 2020 Final]奧運公交(最短路)

[LOJ3255][JOI 2020 Final]奧運公交(最短路)

題面

給出一個\(n\)個點\(m\)條邊的有向圖,經過每條邊需要費用\(c_i\).選擇一條邊並將其反向需要費用\(d_i\)(反向後經過的費用不變).問至多反向一條邊,從1到n再回到1的最小花費

\(n \leq 200,m \leq 50000\)

分析

考慮列舉反向的邊\(i=(u,v)\),經過的費用為\(w\),反轉的費用為\(c\).記\(d(u,v)\)表示\(u\)\(v\)的最少費用,\(f(i,u,v)\)表示不經過邊\(i\)\(u\)\(v\)的最短路
那麼

\[d(1,n)=\min(f(i,1,n),f(i,1,v)+w+f(i,u,n)) \]

\[d(n,1)=\min(f(i,n,1)),f(i,n,v)+w+f(i,u,1)) \]

這是因為從\(1\)\(n\)有2種選擇:不經過\((u,v)\)直接走到\(n\), 或者先到\(v\),經過\((v,u)\)再到\(n\). 從\(n\)\(1\)的情況同理。總費用為\(d(1,n)+d(n,1)+c\)

那麼我們考慮如何求出\(f\).我們發現起點和終點只會是\(1\)\(n\).不妨考慮起點為1的情況。求出\(\operatorname{dist}(i)\)表示原圖起點到\(i\)的費用,並求出一棵最短路樹\(T\)

\[f(i,1,v)=\begin{cases}\operatorname{dist}(v),(u,v)\notin T \\\text{原圖上不經過}i\text{從原點到}v\text{的最小費用},(u,v)\in T\end{cases} \]

對於第一種情況直接開始時預處理即可。第二種情況需要重新跑一次最短路。注意到最短路樹上只有\(n-1\)條邊,第二種情況會出現\(O(n)\)次。如果用堆優化的Dijkstra,單次複雜度為\(O((n+m)\log n)\),因為\(m\)達到了\(n^2\)級別,總複雜度\(O((n^2+nm)\log n)=O(n^3\log n)\),,需要較強的常數優化才能通過。但是無堆優化的Dijkstra複雜度是\(O(n^2+m)\)且常數很小,總複雜度\(O(n^3)\)可以通過本題。

其他的\(f\)同理可求:\(f(i,u,n)\)\(n\)出發在原圖的反圖上跑最短路,\(f(i,n,u)\)

\(n\)出發在原圖上跑最短路,\(f(i,u,1)\)\(1\)出發在原圖的反圖上跑最短路. 程式碼可複用的部分較多,注意封裝。

程式碼

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#define maxm 50000
#define maxn 200
#define INF 0x3f3f3f3f3f3f3f3f 
using namespace std;
typedef long long ll;
int n,m;
struct Graph{
    int S;
    struct edge{
        int from;
        int to;
        int len;
        int cost;
        int next;
    }E[maxm+5]; 
    int head[maxn+5];
    int esz;
    void add_edge(int u,int v,int w,int c){
        esz++;
        E[esz].from=u;
        E[esz].to=v;
        E[esz].len=w;
        E[esz].cost=c;
        E[esz].next=head[u];
        head[u]=esz;
    }

    ll dist1[maxn+5];
    int last[maxn+5];
    bool on_tree[maxm+5]; 
    void dijkstra(){//O(n^2)的Dijkstra,排除m的影響 
        static bool vis[maxn+5];
        memset(dist1,0x3f,sizeof(dist1));
        memset(vis,0,sizeof(vis));
        dist1[S]=0; 
        for(int p=1;p<=n;p++){
            int x=-1;
            for(int i=1;i<=n;i++) if(!vis[i]&&(x==-1||dist1[i]<dist1[x])) x=i;
            if(x==-1) break;
            vis[x]=1;
            for(int i=head[x];i;i=E[i].next){
                int y=E[i].to;
                if(dist1[y]>dist1[x]+E[i].len){
                    dist1[y]=dist1[x]+E[i].len;
                    last[y]=i;
                }
            }
        }
        for(int i=1;i<=n;i++) if(i!=S) on_tree[last[i]]=1;
    }

    ll dist2[maxn+5];
    void dijkstra2(int bane){
        static bool vis[maxn+5];
        memset(dist2,0x3f,sizeof(dist2));
        memset(vis,0,sizeof(vis));
        dist2[S]=0;
        for(int p=1;p<=n;p++){
            int x=-1;
            for(int i=1;i<=n;i++) if(!vis[i]&&(x==-1||dist2[i]<dist2[x])) x=i;
            if(x==-1) break;
            vis[x]=1;
            for(int i=head[x];i;i=E[i].next){
                int y=E[i].to;
                if(i==bane) continue;//由於有重邊,不能用端點來判斷 
                if(dist2[y]>dist2[x]+E[i].len) dist2[y]=dist2[x]+E[i].len;
            } 
        }
    }
    ll calc(int eid,int x){//不經過邊i的1到x最短路 
        if(!on_tree[eid]) return dist1[x];
        else{//重新跑一遍最短路 
            dijkstra2(eid);
            return dist2[x];
        }
    }
}G[4];


int main(){
    static int u[maxm+5],v[maxm+5],w[maxm+5],c[maxm+5];
    scanf("%d %d",&n,&m);
    G[0].S=1;G[1].S=n;G[2].S=1;G[3].S=n;
    for(int i=1;i<=m;i++){
        scanf("%d %d %d %d",&u[i],&v[i],&w[i],&c[i]);
        G[0].add_edge(u[i],v[i],w[i],c[i]); 
        G[1].add_edge(v[i],u[i],w[i],c[i]);//反圖上n的最短路樹,對應正圖上i到n的路徑 
        G[2].add_edge(v[i],u[i],w[i],c[i]);
        G[3].add_edge(u[i],v[i],w[i],c[i]);
    }
    for(int i=0;i<4;i++) G[i].dijkstra();
    ll ans=G[0].dist1[n]+G[3].dist1[1];
    for(int i=1;i<=m;i++){
        ll d1=min(G[0].calc(i,n)/*1直接繞過i到n*/,G[0].calc(i,v[i])/*1->v*/+w[i]+G[1].calc(i,u[i])/*u->n*/);
        ll d2=min(G[3].calc(i,1)/*n直接繞過i到1*/,G[3].calc(i,v[i])/*n->v*/+w[i]+G[2].calc(i,u[i])/*u->1*/);
        if(d1>=INF||d2>=INF) continue;
        ans=min(ans,d1+d2+c[i]);
    } 
    if(ans>=INF) ans=-1;
    printf("%lld\n",ans);
}