。。。
一、最短路径
最短路径是指两顶点之间权值之和最小的路径(有向图、无向图均适用,不能有负权环)。
1.1. 有权图
有向图:
无向图:
1.2. 无权图
无权图相当于是全部边权值为1的有权图。
有向图:
无向图:
1.3. 负权
1.3.1. 负权边
有负权边,但没有负权环时,存在最短路径。
A到E的最短路径是:A → B → E
。
1.3.2. 负权环
有负权环时,不存在最短路径。
通过负权环时,A到E的路径可以无限短:A → E → D → F → E → D → F → E → D → F → E → D → F → E → ......
。
最短路径的典型应用之一:路径规划问题。
求解最短路径的3个经典算法:
- 单源最短路径算法
- Dijkstra(迪杰斯特拉算法)
- Bellman-Ford(贝尔曼-福特算法)
- 多源最短路径算法
- Floyd(弗洛伊德算法)
二、Dijkstra
Dijkstra属于单源最短路径算法(由荷兰的科学家Edsger Wybe Dijkstra发明,曾在1972年获得图灵奖),用于计算一个顶点到其他所有顶点的最短路径。
使用前提:不能有负权边。
时间复杂度:可优化至O(ElogV)
,E
是边数量,V
是节点数量。
Dijkstra的原理其实跟生活中的一些自然现象完全一样。例如,把每1个顶点想象成是1块小石头,每1条边想象成是1条绳子,每一条绳子都连接着2块小石头,边的权值就是绳子的长度。将小石头和绳子平放在一张桌子上(下图是一张俯视图,图中黄颜色的是桌子)。
接下来想象一下,手拽着小石头A,慢慢地向上提起来,远离桌面,B、D、C、E会依次离开桌面,最后绷直的绳子就是A到其他小石头的最短路径。
有一个很关键的信息:后离开桌面的小石头,都是被先离开桌面的小石头拉起来的(虽然是废话,但是代码执行中会不断用到该信息)。
2.1. 执行过程
如下图,就是求源头A到其他点的最短路径。
- A是源头(黑色),下一个被拉起的点必然是E、D、B(红色)其中一个,因为这些点直接连接A,所以更新A点到这几个终点的距离。
- A距离E、D、B最短的是AB(权值10)。此时A和B都已经被拉起,由于后离开桌面的小石头,都是被先离开桌面的小石头拉起来的,所以C和E、D一样都成为下次被拉起之一(更新A到终点C的路径和长度)。
- 注意:由于D还没离开,所以不可能可存在A → D → C。
- A距离C、D、E最短的是AD(权值30)。此时A、B、D都已经被拉起。由于D被拉起,所以出现了新的路径(ADE和ADC)。新的路径比之前路径更短(之前到达E的路径是AE,到达C的路径是ABC),因此更新到达终点E和C的路径和长度。
- A距离C、E最短的是ADC(权值50)。此时A、B、C、D都已经被拉起。由于C被拉起,所以出现了新的路径(ADCE)。新的路径比之前路径更短(之前到达E的路径是ADE),因此更新到达终点E的路径和长度。
- 最终E也被拉起。至此,A点到其他点的最短路径都已经确定。
上面流程3、4都是发现了一条到达一个顶点的更短路径,如点A到达点E最短路径是100(AE),但当D被拉起后,发现可以通过D到达E,并且路径更短(到达E的最短路径是ADE),此时更新到达E的路径长度叫做松弛操作。
松弛操作(Relaxation):更新2个顶点之间的最短路径。这里一般是指更新源点到另一个点的最短路径。
松弛操作的意义:尝试找出更短的最短路径。
对松弛两字的理解:假设两个顶点之间连接一条长度100的绳子,当两点靠拢时,绳子就会出现松弛现象,但是两点之间直线距离在变短(更新最短路径)。
2.2. 具体实现
下面的代码为了方便理解,在找最小路径时没有用到最小堆(用的是循环),所以代码时间复杂度并不是O(ElogV)
,在实际开发中可以把对应方法换成最小堆。
1 | // 图的实现 |
三、Bellman-Ford
Bellman-Ford也属于单源最短路径算法,支持负权边,还能检测出是否有负权环。
算法原理:对所有的边进行V – 1
次松弛操作(V
是节点数量。松弛操作也可能只需要1次),得到所有可能的最短路径。
时间复杂度:O(EV)
,E
是边数量,V
是节点数量。
下图的最好情况是恰好从左到右的顺序对边进行松弛操作(对所有边仅需进行1次松弛操作就能计算出A到达其他所有顶点的最短路径):
最坏情况是恰好每次都从右到左的顺序对边进行松弛操作(对所有边需进行V – 1
次松弛操作才能计算出A到达其他所有顶点的最短路径):
3.1. 执行过程
如上图,源头是A,一共8条边,假设每次松弛操作的顺序是:DC、DF、BC、ED、EF、BE、AE、AB(注意:图的边是用HashSet
存储的,取出的顺序和添加的顺序是不一样的,但只要没有做添加和移除操作,每次取出的顺序都是一样的,也就导致有可能松弛顺序就是这样的)。
能否松弛成功就看起点的最短路径是否已经存在,如果不存在肯定松弛失败。
第1次松弛:由于源头是A,所以只有AE、AB松弛成功。
第2次松弛:BE优先AE做松弛操作,ABE路径长度是5,AE路径长度是8,所以AE松弛操作失败。AB无变化(蓝色表示)。
第3次松弛:更新最短路径和长度。
第4次松弛:
不难分析出,经过4次松弛操作之后,已经计算出了A到其他所有顶点的最短路径(及时后面还有更多次,结果也是一样的)。
3.2. 具体实现
1 | private Map<V, PathInfo<V, E>> bellmanFord(V begin) { |
四、Floyd
Floyd属于多源最短路径算法,能够求出任意2个顶点之间的最短路径,支持负权边。
时间复杂度:O(V^3)
,效率比执行V
次Dijkstra算法要好(每个顶点都作为源点执行一遍Dijkstra算法。V
是顶点数量)。
4.1. 算法原理
从任意顶点i
到任意顶点j
的最短路径不外乎两种可能:
- 直接从
i
到j
; - 从
i
经过若干个顶点到j
。
假设dist(i,j)
为顶点i
到顶点j
的最短路径的距离,对于每一个顶点k
,检查dist(i,k) + dist(k,j) < dist(i,j)
是否成立。
- 如果成立,证明从
i
到k
再到j
的路径比i
直接到j
的路径短,设置dist(i,j) = dist(i,k) + dist(k,j)
- 当我们遍历完所有节点
k
,dist(i,j)
中记录的便是i
到j
的最短路径的距离(每个节点都会成为i、j、k
)
伪代码:
1 | for (int k = 0; k < V; k++) { |
4.2. 具体实现
1 | // 返回的是任意两个顶点之间的路径信息 |