目錄:
從一道題目出發 —— Luogu 4779 - 【模板】單源最短路徑(標准版)
題目鏈接: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 到每個點的距離。
4 6 1 1 2 2 2 3 2 2 4 1 1 3 5 3 4 3 1 4 4
題解:
這是一道最短路的模板題,但是它卡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":" ")); }