图最短路径算法(Graph Shortest Path Algorithm, eg: Floyd-Warshall, Dijkstra, Bellman-Ford, SPFA, Kruskal, Prim, Johnson)
- 最短路径问题有多个衍生问题(并且每个衍生问题都涉及是否有负权边)
单源点最短路径
单终点最短路径
单对顶点最短路径
任意顶点间最短路径
Floyd-Warshall Algorithm
-
适用于多源,可有负权边的有向图的最短路径;时间复杂度为O(V^3),空间复杂度为O(V^2);
-
二维数组path[i][j]表示顶点i到顶点j之间的代价(初始化时由于没有探测他们之间的代价关系,所以为无限大);本算法采取的策略是穷举所有顶点对之间所有可能的中间顶点,并选择代价最小的作为最优解;
1 procedure FloydWarshallWithPathReconstruction () 2 for k := 1 to n 3 for i := 1 to n 4 for j := 1 to n 5 if path[i][k] + path[k][j] < path[i][j] then 6 path[i][j] := path[i][k]+path[k][j]; 7 next[i][j] := k; //next[i][j]表示顶点i和顶点j的中间点 8 9 procedure GetPath (i,j) 10 if path[i][j] equals infinity then 11 return "no path"; 12 int intermediate := next[i][j]; 13 if intermediate equals 'null' then 14 return " "; /* there is an edge from i to j, with no vertices between */ 15 else 16 return GetPath(i,intermediate) + intermediate + GetPath(intermediate,j);
Dijkstra Algorithm
-
适用于有向、无负权边图中,单个源点到其他所有顶点的最短路径问题(Single-Source Shortest Path Problem for Graph with Non-Negative Edge Path Costs)。给定一个带权重的有向图G,V表示所有点的集合,E表示所有边的集合,并且每条边具有权重值w,要求找到给定start点到V中所有其他点 的最短距离。在寻路过程中,如果发现一个新的顶点M,并且从源点到这一个顶点M再到前一个目标顶点的距离比之前的距离小,则更新这个目标定点的最小距离;
-
Dijkstra算法适用于稠密图(边多点少),时间复杂度:使用最小优先队列实现Extract_Min()函数的话,为O(V^2 + E);使用二叉堆实现Extract_Min()函数的话,为O(V^2);使用斐波那契堆(Fibonacci Heap)实现Extract_Min()函数的话,为O(V*lgV + E);Dijkstra算法的一个应用是OSPF(Open Shortest Path First,开放最短路径优先),网络路由寻址的实现
1 function Dijkstra(G, w, s) 2 for each vertex v in V[G] //初始化 3 d[v] := infinity //d[v]存储其他顶点到起始点s的最短距离 4 previous[v] := undefined //previous[v]存储所有顶点的前置节点 5 d[s] := 0 6 S := empty set 7 Q := set of all vertices 8 while Q is not an empty set // Dijkstra演算法主體 9 u := Extract_Min(Q) //Extract_Min()一般使用最小堆实现O(logN) 10 S := S union {u} 11 for each edge (u,v) outgoing from u 12 if d[v] > d[u] + w(u,v) //根据当前节点u的d[u]更新其相邻节点的d[v] 13 d[v] := d[u] + w(u,v) 14 previous[v] := u
Bellman-Ford Algorithm
-
适用于单源、可有负权边的有向图的最短路径,Bellman-Ford对每个顶点都只进行一次处理,所以可以处理负权边,而Dijkstra由于是根据边权值大小选择下一条边,所以负权边可能造成循环;此算法时间复杂度为O(VE),空间复杂度为O(V);
-
数组V[k]表示所有顶点到source的最短距离(初始化为Infinite),并且使用数组P[h]记录所有顶点的前驱,用于记录最短路径;遍历所有 的边集合E内的边e,e连接顶点u和v,判断u和v之间是否可以组成最短路径,这样的遍历重复|V|次;由于V[k]中元素初始化为Infinite,所 以如果如果某个环内存在负权值边,则算法失败;
1 procedure BellmanFord(list vertices, list edges, vertex source) 2 // This implementation takes in a graph, represented as lists of vertices 3 // and edges, and modifies the vertices so that their distance and 4 // predecessor attributes store the shortest paths. 5 6 // Step 1: initialize graph 7 for each vertex v in vertices: 8 if v is source then v.distance := 0 9 else v.distance := infinity 10 v.predecessor := null 11 12 // Step 2: relax edges repeatedly 13 for i from 1 to size(vertices)-1: 14 for each edge uv in edges: // uv is the edge from u to v 15 u := uv.source 16 v := uv.destination 17 if u.distance + uv.weight < v.distance: 18 v.distance := u.distance + uv.weight 19 v.predecessor := u 20 21 // Step 3: check for negative-weight cycles 22 for each edge uv in edges: 23 u := uv.source 24 v := uv.destination 25 if u.distance + uv.weight < v.distance: 26 error "Graph contains a negative-weight cycle"
SPFA (Shortest Path Faster Algorithm)
-
适用于单源、可有负权边的有向图;大多数时候当图存在负权边,Dijkstra不能使用,而Bellman-Ford时间复杂度过高,所以最好使用 SPFA;其实SPFA是Bellman-Ford的优化版本,时间复杂度为O(kE),K是一个远小于V的数字,表示所有顶点进入FQ队列的平均次数 (<=2),但是SPFA及其不稳定,并严重依赖于图数据;
-
SPFA的策略是只有那些在前一次的Relax中的最小距离减小的点,才检查他们的邻接节点的最小距离是否也可减小。SPFA并不会在每一个顶点的最短路 径更新之后就去更新与其相连的顶点,而是等所有顶点都处理完全之后再进行最短路径的更新,这样大大减少重复更改最短路径的次数;所以SFPA中顶点会多次 进入队列,SPFA适合用于稀疏图,此时其拥有最高的效率;
-
数组D[k]存储所有其他顶点到Source的最短距离(初始化为Infinite);数组G[i][j]存储图中直接相连的顶点之间的距离;FIFO队 列Q存储需要进一步优化的顶点,每次从Q中取出顶点u,对每一个u直接相连的顶点v进行Relax操作,如果顶点v的最短距离改变了,并检查v没有在Q 中,则将v加入Q中,之后处理下一个与u直接相连的顶点;检查完u所有直接相连的顶点之后从Q中取出下一个顶点,并进行相同的处理;当Q为空的时候算法结 束,次数D[k]存储的就是图中每一个顶点到source的最短距离;
-
根据将顶点插入到FIFO队列Q的位置不同,SPFA可有两种优化:Small Label First (SLF)和Large Label Last (LLL);但由于SPFA及其不稳定,所以一般情况都使用Dijkstra替代;
1 void spfa(int start){ 2 int i,j; 3 //初始化部分 4 for (i=1;i<=n;++i){ 5 dist[i]=2147483647; 6 inqueue[i]=0; 7 } 8 //将头节点入队 9 dist[start]=0; 10 int h=0,t=1; 11 inqueue[start]=1; 12 queue[1]=start; 13 int now; 14 do{ 15 h++; 16 now=Connect[queue[h]; 17 inqueue[queue[h]=0; 18 while (now){ 19 if (dist[Data[now].v]>dist[queue[h]+Data[now].w){ 20 dist[Data[now].v]=dist[queue[h]+Data[now].w; 21 //进行松弛并扩展被松弛的点 22 if (!inqueue[Data[now].v]){ 23 inqueue[Data[now].v]=1; 24 queue[++t]=Data[now].v; 25 } 26 } 27 now=Pre[now]; 28 } 29 }while (h<t); 30 }
Johnson Algorithm
- 为了让Dijkstra适用于具有负权值边的图,Johnson通过特殊方式将负权值图转换为正权值图,并且为所有顶点之间的最短路径;
- 首先进行一次Bellman-Ford,然后利用等式W(i,j)=h[i]-h[j]+w(i,j);对原图进行重新标号(Re-wrighting, 其实是去除负权值边从而可以使用Dijkstra算法,h[]就是通过Bellman-Ford得到的路径标记,w[][]是边的原始权值);最后对每个 点使用Dijkstra。Johnson是目前在无负权值图中对所有点求最短路径的最高效的算法;