常用最短路算法——-SPFA和Dijkstra及其优化
这篇文章将简单讲解两个最常用的最短路优化算法,需要读者有一定的图论基础。
首先从DIJKSTRA讲起。常规的dijkstra算法复杂度较高,为O(n^2),因为要花大量时间来找当前已知的距顶点距离最小的值,所以用优先队列(值小的先出队列)来优化,可大大优化时间复杂度。STL中优先队列的操作单次复杂度为O(logN),所以经过优先队列优化的dijkstra时间复杂度会降到O(N*logN);
以下为核心部分代码:
1 struct pack{int s,dist;};//利用一个结构体存储节点的起点与到达它的最小距离 2 bool operator<(const pack &a,const pack&b){ 3 return a.dist>b.dist; 4 }//重载pack的小于号,使得大根堆的优先队列为小根堆 5 priority_queue<pack> q; 6 void dij(){ 7 memset(dist,0x7f,sizeof(dist));//初始化最大值 8 memset(vis,0,sizeof(vis)); 9 int s=1; 10 vis[s]=1; 11 dist[s]=0; 12 q.push((pack){s,dist[s]});//将起点放入 13 while(!q.empty()){ 14 pack t=q.top();q.pop();//取出距离最小的点 15 int from=t.s; 16 if(vis[from])continue; 17 vis[from]=1; 18 for(int i=first[from];i;i=edge[i].next){ 19 int to=edge[i].to 20 if(dist[to]>edge[i].val+t.dist){ 21 dist[to]=edge[i].val+t.dist; 22 q.push((pack){to,dist[to]});//松弛操作 23 } 24 } 25 } 26 }
Dijkstra
很多时候,给定的图存在负权边,这时类似Dijkstra算法等便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了。简洁起见,我们约定加权有向图G不存在负权回路,即最短路径一定存在。如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)。
我们用数组dist记录每个结点的最短路径估计值,而且用邻接表来存储图G。我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点from,并且用from点当前的最短路径估计值对离开from点所指向的结点to进行松弛操作,如果to点的最短路径估计值有所调整,且to点不在当前的队列中,就将to点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
期望时间复杂度:O(mE), 其中m为所有顶点进队的平均次数,可以证明m一般小于等于2n;(除非数据点专门卡SPFA,通常m都不大)
以下是队列实现的SPFA模板
queue<int> q; void spfa(int s){ memset(vis,0,sizeof(vis)); memset(dist,0x7f,sizeof(dist)); vis[s]=1; dis[s]=0; q.push(start); while(q.size()){ int from=q.front(); for(int i=first[from];i;i=edge[i].next){ int to=edge[i].to; if(dist[from]+edge[i].val<dist[to]){ dist[to]=dist[from]+edge[i].val; if(!vis[to]){ q.push(to); vis[to]=1; } } } vis[from]=0; q.pop(); } }
SPFA
SPFA算法有两个优化算法 SLF 和 LLL: SLF:Small Label First 策略,设要加入的节点是j,队首元素为i,若dist(j)<dist(i),则将j插入队首,否则插入队尾。 LLL:Large Label Last 策略,设队首元素为i,队列中所有dist值的平均值为x,若dist(i)>x则将i插入到队尾,查找下一元素,直到找到某一i使得dist(i)<=x,则将i出对进行松弛操作。 SLF 可使速度提高 15 ~ 20%;SLF + LLL 可提高约 50%。 在实际的应用中SPFA的算法时间效率不是很稳定,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法。(经过优化的DIJ复杂度稳定在O(N*logN)左右)
下面附利用STL中的双端队列优化的SLF优化代码:
int spfa(int start){ memset(vis,0,sizeof(vis)); memset(dist,0x7f,sizeof(dist)); deque<int> q;//STL中的双端队列 可在两端实现入队出队 vis[start]=1; dist[start]=0; num[start]++; q.push_back(start); while(q.size()){ int from= q.front();q.pop_front();//每次从前端取出 vis[from]=0; for(int i=first[from];i;i=edge[i].next){ int to=edge[i].to; if(dist[from]+edge[i].val<dist[to]){ dist[to]=dist[from]+edge[i].val; if(!vis[to]){ vis[to]=1; num[to]++; if(num[to]==n)return 0;//判断是否有负环存在 if(!q.empty()){ if(dist[to]>dist[q.front()])//距离大的往后端放 q.push_back(to); else q.push_front(to);//小的放前端 } else q.push_back(to); } } } } return 1; }
SPFA SLF
例题 CodeVS 1021 玛丽卡
题目描述 Description
麦克找了个新女朋友,玛丽卡对他非常恼火并伺机报复。
因为她和他们不住在同一个城市,因此她开始准备她的长途旅行。
在这个国家中每两个城市之间最多只有一条路相通,并且我们知道从一个城市到另一个城市路上所需花费的时间。
麦克在车中无意中听到有一条路正在维修,并且那儿正堵车,但没听清楚到底是哪一条路。无论哪一条路正在维修,从玛丽卡所在的城市都能到达麦克所在的城市。
玛丽卡将只从不堵车的路上通过,并且她将按最短路线行车。麦克希望知道在最糟糕的情况下玛丽卡到达他所在的城市需要多长时间,这样他就能保证他的女朋友离开该城市足够远。
编写程序,帮助麦克找出玛丽卡按最短路线通过不堵车道路到达他所在城市所需的最长时间(用分钟表示)。
输入描述 Input Description
第一行有两个用空格隔开的数N和M,分别表示城市的数量以及城市间道路的数量。1≤N≤1000,1≤M≤N*(N-1)/2。城市用数字1至N标识,麦克在城市1中,玛丽卡在城市N中。
接下来的M行中每行包含三个用空格隔开的数A,B和V。其中1≤A,B≤N,1≤V≤1000。这些数字表示在A和城市B中间有一条双行道,并且在V分钟内是就能通过。
输出描述 Output Description
输出文件的第一行中写出用分钟表示的最长时间,在这段时间中,无论哪条路在堵车,玛丽卡应该能够到达麦克处,如果少于这个时间的话,则必定存在一条路,该条路一旦堵车,玛丽卡就不能够赶到麦克处。
样例输入 Sample Input
5 7
1 2 8
1 4 10
2 3 9
2 4 10
2 5 1
3 4 7
3 5 10
样例输出 Sample Output
27
先DIJ记录最短路径的边 然后枚举去掉每一条最短路径的边,DIJ最后取最大值
当然,可以将DIJ换成SPFA,也是同样能AC的
DIJ AC:
#include<bits/stdc++.h> using namespace std; int n,m,cnt,knt; int vis[1010],dist[1010],first[1010],next[600000]; bool ask[1010][1010]; int read(int &x){ //读入优化 char c=getchar();x=0; while(c<‘0‘||c>‘9‘)c=getchar(); while(c>=‘0‘&&c<=‘9‘)x=(x<<3)+(x<<1)+c-‘0‘,c=getchar(); return x; } struct EDGE{ int from,to,val,next; }edge[600000]; struct pack{ int s,dist; }; bool operator<(const pack &a,const pack &b){ return a.dist>b.dist; } void addedge(int a,int b,int c){ edge[++cnt].next=first[a]; edge[cnt].from=a; edge[cnt].to=b; edge[cnt].val=c; first[a]=cnt; } void ini(){ read(n);read(m); int x,y,z; for(int i=1;i<=m;i++){ read(x);read(y);read(z); addedge(x,y,z); addedge(y,x,z); } } void dijclear(){ memset(dist,0x7f,sizeof(dist)); memset(vis,0,sizeof(vis)); } int dij(int s){ dijclear(); priority_queue<pack> q; q.push((pack){s,dist[s]=0}); while(!q.empty()){ pack temp=q.top();q.pop(); int from=temp.s; if(vis[from])continue; vis[from]=1; int k; for(int i=first[from];i;i=edge[i].next){ if(temp.dist+edge[i].val<dist[edge[i].to]){ dist[edge[i].to]=temp.dist+edge[i].val; next[edge[i].to]=from; q.push((pack){edge[i].to,dist[edge[i].to]}); } } } return dist[1]; } int dij2(int s){ dijclear(); priority_queue<pack> q; q.push((pack){s,dist[s]=0}); while(!q.empty()){ pack temp=q.top();q.pop(); int from=temp.s; if(vis[from])continue; vis[from]=1; int k; for(int i=first[from];i;i=edge[i].next){ if(temp.dist+edge[i].val<dist[edge[i].to]&&!ask[from][edge[i].to]){ dist[edge[i].to]=temp.dist+edge[i].val; q.push((pack){edge[i].to,dist[edge[i].to]}); } } } return dist[1]; } int main(){ ini(); int ans=dij(n); for(int i=1;i!=n;i=next[i]){ ask[next[i]][i]=1; ans=max(dij2(n),ans);//将每条边遮住一遍多次求最短路 ask[next[i]][i]=0; } printf("%d",ans); /*int x=1; while(x!=0){ printf("%d->",x); x=next[x]; } */ return 0; }