一个图中,求点u到达点v的最短路径长度,常用的有四种算法:
一、 Floyed算法
可以求出多源最短路,可以处理负权边的情况,但是不能出现负环。
Floyed算法使用的是动态规划的方法。
设d[i][j][k]表示i到j只经过1,2…k这些结点时,i到j的最短路距离。会出现以下两种情况:
(1) 经过k点:
d[i][j][k]=d[i][k][k-1]+d[k][j][k-1]。
(2) 不经过k点:
d[i][j][k]=d[i][j][k-1]。
所以状态转移方程为:
d[i][j][k]=min{d[i][k][k-1]+d[k][j][k-1],d[i][j][k-1]}
边界条件:
d[i][j][0]=w[i][j](连接i到j的边的权值),不存在的边权设为+∞。
因为k是递增的,d[i][j]保存的状态是d[i][j][k-1],所以可以减少一维,使用二维数组。
状态转移方程:
d[i][j]=min{d[i][k]+d[k][j],d[i][j]}。
边界条件:
d[i][j]=w[i][j]。
枚举k,使用中间点k来更新i到j的最短路距离。
【代码实现】
时间复杂度:O(n3)for(k=1;k<=n;k++) //枚举中间 for(i=1;i<=n;i++) //枚举起点 for(j=1;j<=n;j++) //枚举终点 d[i][j]=min(d[i][k]+d[k][j],d[i][j]);
二、Dijkstra算法
求单源最短路径,不能处理负权。
Dijkstra算法使用的是贪心方法,d[i]表示起点s0到i的最短距离。
从起点s0开始,选择未访问过的离s0最近的一个点i,也就是最小的d[i],因为所以边权为正,不会存在更短的路径到达i,保证了贪心的正确性。然后将i作为中间点,更新经过i可到达的点的最短路距离,继续贪心寻找未访问过的最近的一个点,经过n次贪心,算法结束。
【算法步骤】
(1) 初始化d[s0]=0,其他d[i]=+∞。
(2) 经过n次贪心,找到起点s0到其他点的最短路距离
a) 找出一个未访问过的最小的d[k]
b) 标记k被访问过
c) 将k作为中间点,更新起点s0到经过k到达其他点v的d[v]
【图解算法】
【代码实现】
for(i=1;k<=n;k++) { maxn=0x7fffffff; for(j=1;j<=n;j++) //找出未访问最小的d[j] { if( !vis[j] && d[j]<maxn) { maxn=d[j];k=i; } } vis[k]=1; for(j=1;j<=n;j++) //k作为中间点,更新起点经过k到达其他点的d[j] if(w[k][j]) d[j]=min{d[k]+w[k][j],d[j]}; }
时间复杂度:O(n2)
【Dijkstra算法的优化】
由于每次要寻找未访问过的最小的d[i],都要花费O(n)的时间,可以使用堆进行优化。
三、 Bellman-Ford算法
可以求单源最短路径,可以处理负权边的情况,还可以判断是否出现负权回路。
d[i]表示起点s0到i的最短距离。
图的任意一条最短路既不会包含负权回路,也不会包含正权回路,最多包含n-1条边。那么,从源点s可以到达的结点如果存在最短路,则最短路构成了一棵以s为根的最短路树。因此,可以按照距离根s的层次,逐层生成到达每个点的最短路,这就是Bellman-Ford算法的迭代松弛操作。
对每条边进行第一遍松弛操作的时候,生成了从s出发,层数为1的最短路。也就是找到了从s出发,经过1条边到达其他点的最短路(注意此时的最短路是当前层数的最短路,不一定是最终的答案,是一个估计值,可能从第2层某点到达第1层更短,如下图得s-a-b)。对每条边进行第二次松弛操作时,生成了从s出发,层数为2的的最短路,也就是从s出发,经过2条边到达其他点的最短路,以此类推。因为最短路最多含有n-1条边,所以最多进行n-1次松弛操作。
如果经过n-1次松弛后,d[i]还是+∞,说明s不可到达i。
如果经过n-1次松弛后,还能继续松弛,说明有负权回路存在,如果n-1次松弛后,如果有边(u,v)满足d[v]>d[u]+w[u][v],说明还能继续松弛,存在负权回路。
【算法步骤】
(1) 初始化:d[s]=0,d[i]= +∞。
(2) 进行n-1次松弛操作
【图解算法】
【代码实现】
for(i=1;i<=n-1;i++) //n-1次松弛操作 for(j=1;j<=m;j++) //m条边 if( d[edge[j].u]+edge[j].w<d[edge[j].v]) d[edge[j].v]= d[edge[j].u]+edge[j].w; for(j=1;j<=m;j++) if(d[edge[j].u]+edge[j].w<d[edge[j].v]) flag=1; //存在负权回路
实际复杂度:O(n*m)
四、SPFA算法
Bellman-Ford算法中,每次的松弛操作并不需要对所以边进行松弛操作,只需要与当前找到的最短路的点相连的边进行松弛,例如图中,第一次松弛时,只需要松弛(s,a),(s,b),其他边不需要松弛,实际上也无法松弛。因此可以对Bellman-Ford算法进行优化,每次对可以松弛的边进行松弛,不能进行松弛的边不作处理。而SPFA算法就是对Bellman-Ford算法使用队列优化。
设立一个先进先出的优先队列,优先取出队首的点u,用u当前计算出的最短路估计值对边(u,v)进行松弛操作,如果v的最短路估计值有所减小,且v不在队列中,就将队列v入队,放入队尾。这样不断取出结点来进行松弛操作,直到队列为空。
SPFA仍然可以判断是否存在负权回路,如果一个结点弹出/进入队列次数超过了n-1次,说明存在负权回路,可以添加一个数组用来记录每个点的入队次数。
【算法步骤】
(1) 初始化:dis[s]=0,dis[i]= +∞,新建一个队列,将源点s入队,标记s已经入队。
(2) 从队首取出一个点u,标记u已经出队,将与u有边相连的点v进行松弛,,如果松弛成功,判断v是否在队列中,如果没有入队,标记v入队。继续此步骤,直到队列为空。
【代码实现】
q.push(s); vis[s]=1; //源点s入队,标记入队 while(q.size()) { u=q.front();q.pop();vis[u]=0; //取出队头,标记未入队 for(i=head[u];i;i=next[i]) { v=ver[i]; w=edge[i]; if(dis[u]+w<dis[v]) { dis[v]=dis[u]+w; if(!vis[v]) {q.push(v);vis[v]=1;} //如果没有在队列,入队,标记已入队 } } }
时间复杂度:对于稀疏图,为O(km),k为较小的常数,而对于稠密图或者构造的网格图,会提高到O(n*m)。
算法
适用条件
时间复杂度
Floyed算法
求多源最短路,可以处理负权边
O(N^3)
Dijkstra算法
求单源最短路,不能处理负权
O(N^2)
Bellman-Ford算法
求单源最短路,可以处理负权边
O(NM)
SPFA
求单源最短路,可以处理负权边
O(kM)~O(N*M)。