單源最短路問題:OJ5——低德地圖
本題就是一道單源最短路問題。由於是稀疏圖,我們採用Dijkstra演算法。
Dijkstra演算法原理
Dijkstra演算法的步驟
我們把所有的節點分為兩個集合:被選中的(visited==1) 和 未被選中的(visited==0),對於每個點,我們在操作中更新其到源點的距離。這個演算法中用到貪心思想。
我們進行如下操作:
1.在所有未選中的節點中,找出目前距離源點距離最近的點,記為now,並將now移動到“被選中”集合(visited【now】=1).
2.把所有和now相連的節點next的距離進行更新,取dis【now】+l和dis【next】的最小值,從而始終維護dis陣列使得其中儲存了目前選取點集合中的路徑最小值。
3.從s開始迴圈上述步驟直至進行到d。
這樣,我們就得到了最短路的長度dis【d】,同時可以記錄最短路的路徑:只需在上述2步驟中記錄下next節點的最短路是從哪個節點找到了,再回溯回去即可。
Dijkstra貪心的證明
對於貪心演算法,我們在使用的時候應該考慮其正確性。
首先,dijkstra演算法只適用於正邊權圖,圖中不能存在負值邊(因為dijk演算法只能對未被選中的點進行距離更新,而已經選中的點在存在負邊的情況下可能存在更短路)
這裡不給出詳細的證明,證明參見:Dijkstra演算法介紹+正確性證明+效能分析_月本_誠的部落格-CSDN部落格_dijkstra演算法正確性證明
我們只需要知道,對於一條最短路,它的從s1到s2的一部分就是s1到s2的最短路。這樣,前面的演算法就很好理解了。
可以採用歸納法證明,假設在Dijkstra演算法中找到的最短路為V:v1v2.....vn,對於i<k原演算法都正確,而在i=k時不正確。
這時我們考慮到我們在維護陣列時保證了i=k繼承了i=k-1的最短路,因此不可能存在前半部分是最短路,後面不是的情況。
Dijkstra的程式碼實現
這裡直接給出本題的ac程式碼,再分別對不同的坑點進行解釋。
#include<cstdio> #include<cstdlib> #include<vector> #include<queue> #include<utility> using namespace std; typedef pair<int,int> P; struct node{vector<int> next;vector<int> length;}node[30005]; vector<intView Code> path[30005][2]; vector<int> pre[30005][2]; int sum[2],n,m,s,d,pathsum=0; void print(int i){ printf("start\n"); int li=path[1][i].size();for(int j=li-1;j>=0;j--)printf("%d\n",path[1][i][j]); printf("end\n");printf("%d\n",sum[i]); } void push(int now,int i,int ii){ pathsum=i>pathsum?i:pathsum; path[i][ii].push_back(now); int ll=pre[now][ii].size(); for(int j=0;j<ll;j++){ int last=pre[now][ii][j]; if(j)path[i+j][ii].push_back(now); push(last,i+j,ii); } } int dijk(int i){ bool v[30005]={0};int dis[30005];priority_queue<P,vector<P>,greater<P>> calc; for(int i=0;i<=30000;i++){dis[i]=100000000;P x(100000000,i);calc.push(x);} int now;P x(0,s);calc.push(x);dis[s]=0; while(!v[d]){ int min=100000000,minj=0; P x=calc.top();min=x.first;minj=x.second;calc.pop(); while(v[minj]){P x=calc.top();min=x.first;minj=x.second;calc.pop();} now=minj;v[now]=1;if(min>=100000000)return 100000000; int l=node[now].next.size(); for(int j=0;j<l;j++){ int xx=node[now].next[j],ll=node[now].length[j]; P x(dis[now]+ll,xx);calc.push(x); if(dis[now]+ll<dis[xx]){dis[xx]=dis[now]+ll;pre[xx][i].clear();pre[xx][i].push_back(now);} else if(dis[now]+ll==dis[xx])pre[xx][i].push_back(now); } } push(d,1,i);return dis[d]; } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=m;i++){ int x,y,k;scanf("%d%d%d",&x,&y,&k); node[x].next.push_back(y);node[x].length.push_back(k); node[y].next.push_back(x);node[y].length.push_back(k); } scanf("%d%d",&s,&d); sum[1]=dijk(1); for(int q=1;q<=pathsum;q++){ int l=path[q][1].size(); for(int i=1;i<l;i++){ int ss=path[q][1][i],dd=path[q][1][i-1]; int ll=node[ss].next.size();int deltaj=0; for(int j=0;j<ll;j++){if(node[ss].next[j]==dd){deltaj=j;break;}} node[ss].length[deltaj]=100000000; int ll1=node[dd].next.size();int deltaj1=0; for(int j=0;j<ll;j++){if(node[dd].next[j]==ss){deltaj1=j;break;}} node[dd].length[deltaj1]=100000000; } } sum[0]=dijk(0);print(1);if(sum[0]<100000000&&sum[0]>sum[1])print(0); }
首先這道題並不是要求我們找到最短路,而是要求找到所謂的“近似最短路”
它的要求是:不與任何一條最短路的任何一條邊重合
因此我們的思路是:找到所有的最短路,並刪除這些邊。
下面我介紹一下我在做這道題的時候遇到的一些坑點:
如何找到所有的最短路?
我發現在我的dijk演算法中,只能得到一條最短路,因為我起初用一個pre陣列儲存n個頂點在最短路中的上一個頂點,陣列大小開了n,因此每個頂點只能存唯一一個pre點,也就不能得到所有的路。
後來,我在每個頂點處都開了一個vector,來記錄所有可能的pre節點。
int l=node[now].next.size(); for(int j=0;j<l;j++){ int xx=node[now].next[j],ll=node[now].length[j]; P x(dis[now]+ll,xx);calc.push(x); if(dis[now]+ll<dis[xx]){dis[xx]=dis[now]+ll;pre[xx][i].clear();pre[xx][i].push_back(now);} else if(dis[now]+ll==dis[xx])pre[xx][i].push_back(now); }View Code
然後我設計了一個遞迴函式來得到所有最短路中的邊。但我並沒有得到左右最短路,更像是得到了一棵由最短路組成的樹,樹根在終點d處。
void push(int now,int i,int ii){ pathsum=i>pathsum?i:pathsum; path[i][ii].push_back(now); int ll=pre[now][ii].size(); for(int j=0;j<ll;j++){ int last=pre[now][ii][j]; if(j)path[i+j][ii].push_back(now); push(last,i+j,ii); } }View Code
然後我再刪掉這些邊進行dijk尋路就可以了。
如何優化Dijkstra演算法?
然而演算法邏輯正確並不足夠通過oj,目前的Dijkstra演算法複雜度達到了驚人的O(n^2),tle也是必然的。
仔細思考之後,我想到每次尋找最近節點的步驟非常浪費時間:
原始碼:
int min=100000000,minj=0; for(int j=0;j<n;j++){if(min>dis[j]&&!v[j]){min=dis[j],minj=j;}}View Code
而我們完全可以將當前的距離寫進一個優先佇列中,每次在這個最小堆中取最小值就可以了。至於更新的時候,如果有更小的值可以直接加入,無需刪去原來的值,畢竟我們始終只會用到最小的值。
優化後代碼如下:
P x=calc.top();min=x.first;minj=x.second;calc.pop(); while(v[minj]){P x=calc.top();min=x.first;minj=x.second;calc.pop();}View Code
(其實是一個dowhile結構)
然後複雜度就降低到lgn了
彩蛋:附著名oier的ac程式碼如下
#include<bits/stdc++.h> using namespace std; #define N 33333 #define M 833333 #define ll long long #define PI pair<ll,int> #define pb push_back #define mp make_pair priority_queue<PI,vector<PI>,greater<PI>>q; int fir[N],l[M],to[M],w[M],ec=1,ban[M],S,T; void add(int a,int b,int v){l[++ec]=fir[a];fir[a]=ec;w[ec]=v;to[ec]=b;} int n,m,pre[N],sta[N],tp; ll d[N],D[N]; int inSP[N]; void dijk(ll*d,int S,int T,int o){ memset(d,0x3f,N<<3); q.push(mp(d[S]=0,S)); while(q.size()){ int x=q.top().second; ll D=q.top().first; q.pop(); if(D^d[x])continue; for(int i=fir[x],y;i;i=l[i]){ y=to[i]; if(d[y]>D+w[i]&&!ban[i]) q.push(mp(d[y]=D+w[i],y)),pre[y]=x; } } if(o&&d[T]<1e16){ puts("start"); int p=T; while(p^S) sta[++tp]=p=pre[p]; while(tp) printf("%d\n",sta[tp--]); printf("%d\nend\n%lld\n",T,d[T]); } } int main(){ cin>>n>>m; for(int i=0,a,b,W;i<m;++i) scanf("%d%d%d",&a,&b,&W),add(a,b,W),add(b,a,W); cin>>S>>T; dijk(d,S,T,2); dijk(D,T,S,0); for(int i=0;i<n;++i) inSP[i]=d[i]+D[i]==d[T]; for(int i=2,x,y;i<=ec;++i){ x=to[i^1]; y=to[i]; if(inSP[x]&&inSP[y]&&d[y]==d[x]+w[i]) ban[i]=ban[i^1]=1; } dijk(d,S,T,1); }View Code