这里感谢百度文库,百度百科,维基百科,还有算法导论的做者以及他的小伙伴们......前端
最短路是现实生活中很常见的一个问题,以前练习了不少BFS的题目,BFS能够暴力解决不少最短路的问题,可是他有必定的局限性,该算法只能用于无权重即权重为单位权重的图,那么下面咱们会介绍五种用途更普遍的算法......算法
最短路径的几个变体数组
单源最短路径问题:咱们但愿找到从源结点s到其它全部结点的最短路径。闭包
单目的地最短路径问题:找到从每一个结点v到目的u的最短路径,若是将图中每条边的方向翻转过来,咱们就能够将这个问题转换为单源最短路径问题。函数
单结点对最短路径问题:该类问题的求解方法和单源最短路径类似,相比之下还有更多的优化算法。oop
全部结点对的最短路径问题:对于每一个结点u和v,找到从结点u到v的最短路径。虽然能够对每一个结点运行一次单源最短路径算法,可是咱们有更好的算法来解决这类问题,就是后面讲到的Floyd-Warshall算法。性能
最短路知足最优子结构,最优子结构是运用贪心算法和动态规划的一个重要指标,咱们即将了解的Dijkstra算法就是一个贪心算法,Floyd-Warshall算法就是一个动态规划算法,下面咱们贴出一段算法导论上面对该定理的证实。优化
通常用于解决单源最短路问题,这里权值能够为负值,给定一个有向图G(V, E)和一个权重函数w : E -> R,该算法返回一个布尔值,以代表是否存在一个从源结点能够到达的权重为负值的环路。若是存在这样的回路,spa
该算法将告诉咱们不存在解决方案,不然该算法会给出最短路径和他们的权重。3d
该算法经过对边进行松弛操做来逐渐下降从源结点到每一个结点v的最短路径的估计值v.d,直到得到最短路径为止。
下面给出算法导论上面的伪代码,因为在进行该算法以前要对每一个结点的初始状态进行初始化,因此给出一个初始化操做。
初始化操做就是对于每一个结点,将其到全部结点的最短路径初始化为正无穷,将他们的父亲结点初始化为NULL,并将源结点到本身的最短路径初始化为0。
松弛操做就是对于每条边,若是发现当前边终点处对应的最短路径v.d比起点处u.d + w(u, v) 大,则将其更新,因为最短路径问题知足最优子结构,并在最短路径树中将u记录为v的父亲结点。
整个Bellman-Ford算法就是先对图中的结点进行初始化,而后对于每一个结点v进行松弛操做,松弛时对每一个v为起点的边进行松弛操做,以后再检查是否知足最短路问题的最优子结构性质,若是发现违背这个性质,则说明图中存在负权环。
下面咱们介绍一种Bellman-Ford算法的优化算法SPFA算法......
SPFA算法全称为Shortest Path Fast Algorithm,在1994年由西南交通大学段凡丁提出,与Bellman-Ford算法同样,用于求解含负权的最短路问题以及判断是否存在负权环。在不含负权环的题状况下优先选择堆优化的Dijkstra算法求最短路径,这就避免SPFA出现最坏的状况。SPFA算法的基本思路与Bellman-Ford算法相同,即每一个节点都被用做用于松弛其相邻节点的备选节点。相较于Bellman-Ford算法,SPFA算法的提高在于它并不盲目尝试全部节点,而是维护一个备选节点队列,而且仅有节点被松弛后才会放入队列中。整个流程不断重复直至没有节点能够被松弛。
下面咱们给出维基上SPFA算法的伪代码:
// w(u,v)是边(u,v)}的权。 procedure Shortest-Path-Faster-Algorithm(G, s) 1 for each vertex v ≠ s in V(G) 2 d(v) := ∞ 3 d(s) := 0 4 offer s into Q 5 while Q is not empty 6 u := poll Q 7 for each edge (u, v) in E(G) 8 if d(u) + w(u, v) < d(v) then 9 d(v) := d(u) + w(u, v) 10 if v is not in Q then 11 offer v into Q
SPFA算法的性能很大程度上取决于用于松弛其余节点的备选节点的顺序。事实上,若是Q是一个优先队列,则这个算法将极其相似于Dijkstra算法。然而尽管这一算法中并无用到优先队列,仍有两种可用的技巧能够用来提高队列的质量,而且借此可以提升平均性能(但仍没法提升最坏状况下的性能)。两种技巧经过从新调整Q中元素的顺序从而使得更靠近源点的节点可以被更早地处理。所以一旦实现了这两种技巧,Q将再也不是一个先进先出队列,而更像一个链表或双端队列。
下面咱们说一下两种优化的思路以及维基上提供的伪代码:
Small Lable First :他的意思是距离小者优先,也就是咱们有入队元素时,咱们判断队尾元素的权值是否小于队头元素权值,若是知足则将队尾元素剔除并将插到队首。
下面咱们给出维基百科上的伪代码,把这一段代码加到上述SPFA代码的第11行以后便可:
1 procedure Small-Label-First(G, Q) 2 if d(back(Q)) < d(front(Q)) then 3 u := pop back of Q 4 push u into front of Q
Large Lable Last:这种优化思路是距离大者置后,也就是咱们每次都会计算出当前队列中元素的平均值,当发现正在入队的元素的权值大于平均权值时就将它从队首剔除并插入到队尾。
下面咱们给出维基百科上的伪代码,把这一段代码加到上述SPFA代码的第11行以后便可:
1 procedure Large-Label-Last(G, Q) 2 x := average of d(v) for all v in Q 3 while d(front(Q)) > x 4 u := pop front of Q 5 push u to back of Q
上述算法便是单源最短路含负权问题的通常解法,下面咱们描述一种对于有向无环图上的最短路问题的解法,该解法内求解单源最短路问题的时间复杂度为O(V + E)。
在有向无环图中,即便某个边的权重为负值,若是图中不存在起点s可到达的权重为负值的环,那么其最短路径都是存在的。
咱们的算法首先对有向无环图进行拓扑排序,以便肯定结点之间的一个线性次序。若是图中包含从u到v的一条最短路,那么在有向无环图的拓扑序中,u结点必定位于v结点的前面。所以咱们只须要按照拓扑序 对结点进行一遍处理便可。
每次对一个结点进行处理时,咱们对从该结点出发的全部边进行松弛操做。
松弛操做和初始化操做与Bellman-Ford算法中的伪代码彻底同样,下面给出算法导论上面关于拓扑排序和DAG-SHORTEST-PATHs的伪代码:
算法导论中的拓扑排序依靠对于每一个边记录DFS完成顺序的结果进行的,因此咱们对该DFS和DFS造成的拓扑序进行讲解。
对于每一个结点,用白色表示它尚未访问过,用灰色表示正在访问,用黑色表示访问完毕,那么对于任意一个图,咱们首先对图中的结点进行初始化,将全部结点的父亲结点初始化为NULL,将全部结点的颜色都初始化为白色表示未
访问,接着顺序遍历每一个结点,若是结点没有被访问果咱们就访问该结点,访问每一个结点时,咱们设置一个全局变量的计时器存储每一个结点被访问完毕的时间,咱们对该结点的其它邻接结点进行访问,若是邻接结点为白色则对其进行相同
的访问,并将它的父亲标记为当前结点,访问完一个结点的全部邻接结点时咱们将其状态变为黑色,并记录此时的访问完成时间用于拓扑排序。
如何求出拓扑序呢,首先利用DFS计算出每一个结点最后一次访问的时间,对于图中的每一个结点,访问完毕后直接将其结点的编号加入到链表的最前端,最后返回头结点。那么要如何证实一个DFS对于一个结点访问完毕的顺序就是逆拓扑序呢?
首先咱们知道,对于一个图G,若是他是有向无环图,那么图中必定存在出度为0的结点,这个出度为零的结点一定是最后访问的。那么在DFS访问的过程当中,若是一个结点的全部子节点都被访问完毕以后,其出度即为零,即DFS对结点的
访问完毕顺序知足逆拓扑序。
那么要如何实现DAG-SHORTEST-PATHS呢,咱们先将全部结点拓扑排序,接着对结点的信息状态进行初始化,接着按照拓扑序对每一个结点的邻接结点进行松弛操做,便可完成。
Dijkstra算法解决的是带权重的有向图上单源最短路问题,该算法要求全部边的权重都为非负值。若是方式适当,Dijkstra算法的运行时间要优于SPFA算法的运行时间。
Dijkstra算法会维护一组关键的信息,用于下来的运算,从源结点s到该集合中每一个结点的最短路径已经被找到,算法重复从V-S中选择最短路径估计最小的结点u,将u加入到集合S中,而后对全部从u出发的边进行松弛操做。
该算法之因此是正确的是由于每次选择结点u加入S中时,有u.d = shortest-paths。那么要如何证实在结点u加入集合s时有上述性质呢?......算法导论上面花了较长的篇幅进行证实,这里再也不进行赘述。
这里是用最小优先队列Q来保存结点集合。首先咱们对每一个结点进行相同于Bellman-Ford算法的初始化,接着将集合s初始化为一个空集,算法第11行将全部结点放入该队列中,对于该队列中的全部结点,咱们每次访问其中
权值最小的那个结点u,并将其加入S中,接着对其结点进行松弛,松弛操做与Bellman-Ford算法相同。
下面给出维基百科上Dijkstra算法关于优先队列优化的伪代码:
1 function Dijkstra(Graph, source): 2 dist[source] ← 0 // Initialization 3 4 create vertex set Q 5 6 for each vertex v in Graph: 7 if v ≠ source 8 dist[v] ← INFINITY // Unknown distance from source to v 9 prev[v] ← UNDEFINED // Predecessor of v 10 11 Q.add_with_priority(v, dist[v]) 12 13 14 while Q is not empty: // The main loop 15 u ← Q.extract_min() // Remove and return best vertex 16 for each neighbor v of u: // only v that are still in Q 17 alt ← dist[u] + length(u, v) 18 if alt < dist[v] 19 dist[v] ← alt 20 prev[v] ← u 21 Q.decrease_priority(v, alt) 22 23 return dist, prev
上面的代码和算法导论给出的伪代码思路相同,可是实现方式有必定的差距,该算法和算法导论中Dijkstra算法伪代码不一样的地方为,该算法第一次只将起始点加入优先队列,而后在每次进行松弛操做的时候将正在松弛的点加入优先队列用于计算,算法导论中伪代码是将全部顶点先加入优先队列,因为刚开始时只有源点s的权值为0,因此该点就是所采用的第一个点,接着循环进行松弛操做时改变相应的值,因为优先队列会从新平衡,因此这样也并没有大碍,可是建议仍是采用上面代码而非一次将全部结点都加入优先队列,由于这会使得该算法变得笨重些许......(堆每次须要调整使本身平衡)。下面给出算法导论中对于Dijkstra算法的伪代码实现。
Floyd-Warshall算法经过逐步改进两个顶点之间的最短路径来实现,直到估计是最优的。
是解决任意两点间的最短路径的一种算法,能够正确处理有向图或负权(但不可存在负权回路)的最短路径问题,同时也被用于计算有向图的传递闭包[2]。
Floyd-Warshall算法的时间复杂度为O(N3),空间复杂度为O(N2)。
原理:Floyd-Warshall算法的原理是动态规划。
设D(i, j, k)为从i到j的只以(1....k)集合中的节点为中间节点的最短路径的长度。
所以,D(i, j, k) = min(, D(i, j, k - 1), D(i, k, k - 1) + D(k, j, k - 1));。
在实际算法中,为了节约空间,能够直接在原来空间上进行迭代,这样空间可降至二维。
下面给出维基百科上面的伪代码描述:
用一个next数组保存路径,意味着以结点 i 为开头的一颗最短路径树。
1 let dist be a |V| * |V| array of minimum distances initialized to 2 let next be a |V| * |V| array of vertex indices initialized to null 3 4 procedure FloydWarshallWithPathReconstruction () 5 for each edge (u,v) 6 dist[u][v] ← w(u,v) // the weight of the edge (u,v) 7 next[u][v] ← v 8 for each vertex v 9 dist[v][v] ← 0 10 next[v][v] ← v 11 for k from 1 to |V| // standard Floyd-Warshall implementation 12 for i from 1 to |V| 13 for j from 1 to |V| 14 if dist[i][j] > dist[i][k] + dist[k][j] then 15 dist[i][j] ← dist[i][k] + dist[k][j] 16 next[i][j] ← next[i][k]
后续还会更新有关Johnson算法得有关内容,而且更新Floyd-Warshall算法得内容。
这几天会有持续的最短路题目更新emmm.....