單源最短路進階 - “舊王已死,新王當立!”


目錄:

從一道題目出發 —— Luogu 4779 - 【模板】單源最短路徑(標准版)

Bellman-Ford算法

SPFA算法

SLF+swap優化的SPFA(但它還是死了

Dijkstra算法

優先隊列優化Dijkstra算法

手寫二叉堆優化Dijkstra算法

線段樹優化Dijkstra算法

配對堆優化Dijkstra算法

 


題目鏈接:https://www.luogu.org/problemnew/show/P4779

題目背景

2018 年 7 月 19 日,某位同學在 NOI Day 1 T1 歸程 一題里非常熟練地使用了一個廣為人知的算法求最短路。

然后呢?

100→60;

Ag→Cu;

最終,他因此沒能與理想的大學達成契約。

小 F 衷心祝願大家不再重蹈覆轍。

題目描述

給定一個 N 個點,M 條有向邊的帶非負權圖,請你計算從 S 出發,到每個點的距離。

數據保證你能從 S 出發到任意點。

輸入格式:

第一行為三個正整數 N,M,S。 第二行起 M 行,每行三個非負整數 ui​,vi​,wi​,表示從 ui​ 到 vi​ 有一條權值為 wi​ 的邊。

輸出格式:

輸出一行 N 個空格分隔的非負整數,表示 S 到每個點的距離。

輸入樣例#1:
4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
輸出樣例#1:
0 2 4 3

 

題解:

這是一道最短路的模板題,但是它卡SPFA,還卡某些優化不好的SPFA,所以本題我們要使用SLF+swap優化的SPFA。

(順便再用堆優化dijkstra)

 


首先回顧一下Bellman-Ford算法:

①初始化,所有點的 dist[i] = INF,出發點 s 的 dist[s] = 0;

②對於每條邊 edge(u,v),若 dist[u] != INF,且 dist[v] > dist[u] + edge(u,v).w,則松弛 dist[v] = dist[u] + edge(u,v).w

③循環步驟② $\left| V \right| - 1$ 次,或者知道某一次步驟②中沒有邊可以松弛,則轉步驟④

④若存在一條邊 edge(u,v),滿足 dist[u] != INF,且dist[v] > dist[u] + edge(u,v).w,則圖中存在負環。

 

我們知道,Bellman-Ford算法的時間復雜度是 $O\left( {\left| V \right|\left| E \right|} \right)$,而我們可以使用隊列對其進行優化,那就是大名鼎鼎的SPFA算法,

所以說,SPFA就是隊列優化的Bellman-Ford算法。

 


不妨回顧一下SPFA算法:

①初始化,所有點的 dist[i] = INF,源點 s 的 dist[s] = 0;構建隊列,源點 s 入隊,並標記該點已在隊列中。

②隊頭出隊,標記該點已不在隊列中(若圖存在負權邊,則可以對該點出隊次數檢查,若出隊次數大於 n,則存在負環,算法結束),

 遍歷該點出發的所有邊,假設當前遍歷到某條邊為 edge(u,v),若 dist[v] > dist[u] + edge(u,v).w,則松弛dist[v] = dist[u] + edge(u,v).w,

 檢查節點 v 是否在隊列中,若不在則入隊,標記節點 v 已在隊列中。

④重復執行步驟②直到隊列為空。

 

普通SPFA的TLE代碼:

#include<bits/stdc++.h>
using namespace std;

const int maxn=1e5+10;
const int INF=0x3f3f3f3f;

int n,m,s;

//鄰接表存圖
struct Edge{
    int u,v,w;
    Edge(int u=0,int v=0,int w=0){this->u=u,this->v=v,this->w=w;}
};
vector<Edge> E;
vector<int> G[maxn];
void addedge(int u,int v,int w)
{
    E.push_back(Edge(u,v,w));
    G[u].push_back(E.size()-1);
}

//SPFA單源最短路
int dist[maxn];
bool vis[maxn];
void spfa()
{

    for(int i=1;i<=n;i++) dist[i]=INF,vis[i]=0;
    dist[s]=0;

    queue<int> Q;
    Q.push(s); vis[s]=1;
    while(!Q.empty())
    {
        int u=Q.front();Q.pop(); vis[u]=0;
        for(int i=0;i<G[u].size();i++)
        {
            Edge &e=E[G[u][i]];
            int v=e.v;
            if(dist[v]>dist[u]+e.w)
            {
                dist[v]=dist[u]+e.w;
                if(!vis[v])
                {
                    Q.push(v); vis[v]=1;
                }
            }
        }
    }
}

int main()
{
    scanf("%d%d%d",&n,&m,&s);
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        addedge(u,v,w);
    }
    spfa();
    for(int i=1;i<=n;i++) printf("%d%s",dist[i],((i==n)?"\n":" "));
}

  

 

但是,對於這個“隊列優化”,有必要清楚的一點是:

SPFA的時間復雜度,其實和Bellman-Ford是一樣的,都是$O\left( {\left| V \right|\left| E \right|} \right)$,

只是SPFA在部分圖中跑的比較快,給人以 $O\left( {k\left| E \right|} \right)$ 的感覺(其中 $k$ 為所有點入隊次數的平均,部分圖的 $k$ 值很小),

但是,現在很多的題目,都是會卡掉SPFA的。所以,現在對於沒有負權邊的圖,單源最短路請優先考慮堆優化Dij

當然啦,SPFA被卡了我還是想用SPFA怎么辦?根據知乎上@fstqwq對於“如何看待SPFA算法已死這種說法?”的回答表明,

在不斷的構造圖卡SPFA和不斷地優化SPFA過數據的斗爭中,LLL優化、SLF優化、SLF帶容錯等一系列優化都被卡掉了,

所以……

而到目前(2018.9.4)為止,暫時有位神仙想出了一種SLF+swap優化的SPFA,暫時還很難卡掉,是不是SPFA還能苟住一波呢?心向往之情不自禁地就想了解一下:

 


首先是單純的 SLF優化:Small Label First策略,設要入隊的節點是 j,而隊首元素為 i,若dist[j] < dist[i] 則將 j 插入隊首,否則插入隊尾。

再然后是 SLF+swap優化:每當隊列改變時,如果隊首節點 i 的 dist[i] 大於隊尾節點 j 的 dist[j],則交換首尾節點。

 

SLF+swap優化的AC代碼:

#include<bits/stdc++.h>
using namespace std;

const int maxn=1e5+10; const int INF=0x3f3f3f3f; int n,m,s; //鄰接表存圖 struct Edge{ int u,v,w; Edge(int u=0,int v=0,int w=0){this->u=u,this->v=v,this->w=w;} }; vector<Edge> E; vector<int> G[maxn]; void addedge(int u,int v,int w) { E.push_back(Edge(u,v,w)); G[u].push_back(E.size()-1); } //數組模擬隊列 const int Qsize=2e5+10; int head,tail; int Q[Qsize]; //SPFA單源最短路 int dist[maxn]; bool vis[maxn]; void spfa() { for(int i=1;i<=n;i++) dist[i]=INF,vis[i]=0; dist[s]=0; head=tail=0; Q[tail++]=s; vis[s]=1; while(head<tail) { int u=Q[head++]; vis[u]=0; if(head<tail-1 && dist[Q[head]]>dist[Q[tail-1]]) swap(Q[head],Q[tail-1]); for(int i=0;i<G[u].size();i++) { Edge &e=E[G[u][i]]; int v=e.v; if(dist[v]>dist[u]+e.w) { dist[v]=dist[u]+e.w; if(!vis[v]) { Q[tail++]=v; vis[v]=1; if(head<tail-1 && dist[Q[head]]>dist[Q[tail-1]]) swap(Q[head],Q[tail-1]); } } } } } int main() { scanf("%d%d%d",&n,&m,&s); for(int i=1;i<=m;i++) { int u,v,w; scanf("%d%d%d",&u,&v,&w); addedge(u,v,w); } spfa(); for(int i=1;i<=n;i++) printf("%d%s",dist[i],((i==n)?"\n":" ")); }

第二天凌晨(2018.9.5 - 0:18)更新,上面的代碼WA了,添加的第六組數據,直接把這種優化的SPFA給叉掉了,神奇!

所以!再強調一遍!

所以!還是Dijkstra大法好!

 

 


 接下來回到Dijkstra時間!之前說了,沒有負權邊的圖,推薦使用堆優化的Dijkstra,時間復雜度有保證!

 

首先依然是回顧一下Dijkstra算法:

①構建兩個存儲節點的集合:

 集合S:存儲的是已經確定正確計算出dist[]的節點,剛開始為空;

 集合Q:$V - S$,剛開始時就等於集合V。

構建標記數組vis[],標記為1代表該點在集合S中,標記為0就在集合Q中。

②初始化,所有點的 dist[i] = INF,源點 s 的 dist[s] = 0;所有點的標記全部置零。

②重復 $\left| V \right|$ 次如下步驟:

 1.尋找集合Q里dist[]最小的那個節點 u,標記 vis[u] = 1(放入集合S中)

 2.遍歷節點 u 出發的所有邊,假設當前遍歷到某條邊為 edge(u,v),若節點 v 在集合Q中(vis[v] = 0),則嘗試松弛dist[v] = min( dist[v] , dist[u] + edge(u,v).w )。

 

普通的Dijkstra算法時間復雜度 $O\left( {\left| V \right|^2 } \right)$,跟Bellman-Ford算法和普通SPFA一樣過不了本題,

所以就要掏出堆優化的Dijkstra了,因為要尋找集合Q里dist[]最小的那個節點 u,不一定要遍歷來尋找,可以通過堆來降低尋找的時間復雜度。

 


第一種,實現起來最簡單的,用STL庫的優先隊列實現,

考慮最壞情況,所有的邊都要松弛一遍,則往優先隊列里push了 $O\left( {\left| E \right|} \right)$ 個元素,所以每次push和pop都要 $O\left( {\log \left| E \right|} \right)$,

同樣,又因為最壞情況每條邊都要松弛一次,則要進行 $O\left( {\left| E \right|} \right)$ 次push和pop。故時間復雜度 $O\left( {\left| E \right|\log \left| E \right|} \right)$,

AC代碼:

#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii; //first是最短距離,second是節點編號
#define mk(x,y) make_pair(x,y)
const int maxn=1e5+10;
const int INF=0x3f3f3f3f;

int n,m,s;
struct Edge{
    int u,v,w;
    Edge(int u=0,int v=0,int w=0){this->u=u,this->v=v,this->w=w;}
};
vector<Edge> E;
vector<int> G[maxn];
void addedge(int u,int v,int w)
{
    E.push_back(Edge(u,v,w));
    G[u].push_back(E.size()-1);
}

int dist[maxn]; bool vis[maxn];
priority_queue< pii, vector<pii>, greater<pii> > Q;
void dijkstra()
{
    for(int i=1;i<=n;i++) dist[i]=INF, vis[i]=0;
    dist[s]=0, Q.push(mk(0,s));
    while(!Q.empty())
    {
        int u=Q.top().second; Q.pop();
        if(vis[u]) continue;
        vis[u]=1;
        for(auto x:G[u])
        {
            Edge &e=E[x]; int v=e.v;
            if(vis[v]) continue;
            if(dist[v]>dist[u]+e.w) dist[v]=dist[u]+e.w, Q.push(mk(dist[v],v));
        }
    }
}

int main()
{
    scanf("%d%d%d",&n,&m,&s);
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        addedge(u,v,w);
    }
    dijkstra();
    for(int i=1;i<=n;i++) printf("%d%s",dist[i],((i==n)?"\n":" "));
}

 


接下來是手寫二叉堆優化Dijkstra算法,由於控制堆內元素個數 $O\left( {\left| V \right|} \right)$,所以每次push和pop時間復雜度是 $O\left( {\log \left| V \right|} \right)$,

同時,每個點都出堆(或者說,出集合Q)一次,則進行了 $O\left( {\left| V \right|} \right)$ 次pop操作,

又考慮最壞情況每條邊都進行了松弛,則進行了 $O\left( {\left| E \right|} \right)$ 次入堆push或者堆內某個點上移up操作,

因此總時間復雜度 $O\left( {\left( {\left| V \right| + \left| E \right|} \right)\log \left| V \right|} \right)$。

AC代碼:

#include<bits/stdc++.h>
using namespace std;

const int maxn=1e5+10;
const int INF=0x3f3f3f3f;

int n,m,s;
int dist[maxn];
bool vis[maxn];

struct Edge{
    int u,v,w;
    Edge(int u=0,int v=0,int w=0){this->u=u,this->v=v,this->w=w;}
};
vector<Edge> E;
vector<int> G[maxn];
void addedge(int u,int v,int w)
{
    E.push_back(Edge(u,v,w));
    G[u].push_back(E.size()-1);
}

struct Heap
{
    int sz;
    int heap[4*maxn],pos[maxn];
    void up(int now)
    {
        while(now>1)
        {
            int par=now>>1;
            if(dist[heap[now]]<dist[heap[par]]) //子節點小於父節點,不滿足小頂堆性質
            {
                swap(heap[par],heap[now]);
                swap(pos[heap[par]],pos[heap[now]]);
                now=par;
            }
            else break;
        }
    }
    void push(int x) //插入權值為x的節點
    {
        heap[++sz]=x;
        pos[x]=sz;
        up(sz);
    }
    inline int top(){return heap[1];}
    void down(int now)
    {
        while((now<<1)<=sz)
        {
            int nxt=now<<1;
            if(nxt+1<=sz && dist[heap[nxt+1]]<dist[heap[nxt]]) nxt++; //取左右子節點中較小的
            if(dist[heap[now]]>dist[heap[nxt]]) //子節點小於父節點,不滿足小頂堆性質
            {
                swap(heap[now],heap[nxt]);
                swap(pos[heap[now]],pos[heap[nxt]]);
                now=nxt;
            }
            else break;
        }
    }
    void pop() //移除堆頂
    {
        heap[1]=heap[sz--];
        pos[heap[1]]=1;
        down(1);
    }
    void del(int p) //刪除存儲在數組下標為p位置的節點
    {
        heap[p]=heap[sz--];
        pos[heap[p]]=p;
        up(p), down(p);
    }
    inline void clr()
    {
        sz=0;
        memset(pos,0,sizeof(pos));
    }
}h;

void dijkstra()
{
    for(int i=1;i<=n;i++) dist[i]=INF, vis[i]=0;
    dist[s]=0;

    h.clr();
    h.push(s);
    while(h.sz)
    {
        int u=h.top(); h.pop();
        if(vis[u]) continue;
        vis[u]=1;
        for(int i=0;i<G[u].size();i++)
        {
            Edge &e=E[G[u][i]]; int v=e.v;
            if(!vis[v] && dist[v]>dist[u]+e.w)
            {
                dist[v]=dist[u]+e.w;
                if(h.pos[v]) h.up(h.pos[v]);
                else h.push(v);
            }
        }
    }
}

int main()
{
    scanf("%d%d%d",&n,&m,&s);
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        addedge(u,v,w);
    }
    dijkstra();
    for(int i=1;i<=n;i++) printf("%d%s",dist[i],((i==n)?"\n":" "));
}

這個代碼還沒有AC,只拿了80分,但是今晚(2018.9.4 - 23:38)把上面AC過的代碼交了一發,以及本題題解里面的代碼交了一些,都只有80分,最后一個測試點沒能通過,原因是因為Too long on line 1,比較奇怪,猜測可能是數據問題。

第二天凌晨(2018.9.5 - 0:15)更新,上面的代碼AC了,添加的第六組數據有點問題,已經被@fstqwq巨巨修好了。

 


當然,最后還有一種比較神奇的優化Dijkstra方式,就是線段樹優化(線段樹天下第一!),

其實它優化Dijkstra的原理和優先隊列和二叉堆都是差不多的,優化的重點無非是在集合Q找dist[]最小的那個點,

所以首先不妨把1~n個點全部扔進去建線段樹,維護兩個值:區間最小值minval 和 最小值在哪個位置minpos,現在這棵線段樹就是我們的初始的集合Q了!

要在集合Q里找dist[]最小的節點 u,簡單啊 節點u 不就是 node[root].minpos 嘛!

很好,那接下來怎么把這個節點從集合Q里踢出去呢,刪除節點不現實,把它更新成INF不就好了,這樣以后就不會再找到這個點了,

如果還能再找到這個點……說明整棵線段樹里所有元素的值都變成INF了,那不就代表集合Q是空的了嘛,所以循環結束~

時間復雜度:建樹 $O\left( {\left| V \right|} \right)$,線段樹單點修改 $O\left( {\log \left| V \right|} \right)$,

      每個點出集合Q一次即 $O\left( {\left| V \right|} \right)$ 次線段樹單點修改,每條邊全部松弛一次即 $O\left( {\left| E \right|} \right)$ 次線段樹單點修改,

      因此總的時間復雜度 $O\left( {\left( {\left| V \right| + \left| E \right|} \right)\log \left| V \right|} \right)$。

#include<bits/stdc++.h>
using namespace std;

const int maxn=1e5+10;
const int INF=0x3f3f3f3f;

int n,m,s;
int dist[maxn];
bool vis[maxn];

struct Edge{
    int u,v,w;
    Edge(int u=0,int v=0,int w=0){this->u=u,this->v=v,this->w=w;}
};
vector<Edge> E;
vector<int> G[maxn];
void addedge(int u,int v,int w)
{
    E.push_back(Edge(u,v,w));
    G[u].push_back(E.size()-1);
}

/********************************* Segment Tree - st *********************************/
struct Node{
    int l,r;
    int minval,minpos;
}node[4*maxn];
int nodeidx[maxn];
void pushup(int root)
{
    if(node[root<<1].minval<=node[root<<1|1].minval)
    {
        node[root].minval=node[root<<1].minval;
        node[root].minpos=node[root<<1].minpos;
    }
    else
    {
        node[root].minval=node[root<<1|1].minval;
        node[root].minpos=node[root<<1|1].minpos;
    }
}
void build(int root,int l,int r)
{
    if(l>r) return;
    node[root].l=l; node[root].r=r;
    if(l==r)
    {
        node[root].minval=((l==s)?0:INF);
        node[root].minpos=l;
        nodeidx[l]=root;
    }
    else
    {
        int mid=l+(r-l)/2;
        build(root*2,l,mid);
        build(root*2+1,mid+1,r);
        pushup(root);
    }
}
void update(int root,int pos,int val)
{
    if(node[root].l==node[root].r)
    {
        node[root].minval=val;
        return;
    }

    int mid=node[root].l+(node[root].r-node[root].l)/2;
    if(pos<=mid) update(root*2,pos,val);
    if(pos>mid) update(root*2+1,pos,val);
    pushup(root);
}
/********************************* Segment Tree - ed *********************************/

void dijkstra()
{
    for(int i=1;i<=n;i++) dist[i]=((i==s)?0:INF),vis[i]=0;

    build(1,1,n);
    while(node[1].minval<INF)
    {
        int u=node[1].minpos;
        if(vis[u]) continue;
        vis[u]=1; update(1,u,INF);
        for(int i=0;i<G[u].size();i++)
        {
            Edge &e=E[G[u][i]]; int v=e.v;
            if(vis[v]) continue;
            if(dist[v]>dist[u]+e.w)
            {
                dist[v]=dist[u]+e.w;
                update(1,v,dist[u]+e.w);
            }
        }
    }
}

int main()
{
    scanf("%d%d%d",&n,&m,&s);
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        addedge(u,v,w);
    }
    dijkstra();
    for(int i=1;i<=n;i++) printf("%d%s",dist[i],((i==n)?"\n":" "));
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM