一、弗洛伊德算法概述
在上一篇博客 图解迪杰斯特拉算法(最短路径问题) 中介绍了迪杰斯特拉算法,该算法用于求解单源最短路径问题。所谓单源最短路径路径就是从某一个顶点出发,求解其到各个顶点的最短路径。
那么如果想要求解每一对顶点之间的最短路径,该怎么做呢?
其实可以对迪杰斯特拉进行简单的改造,就可以实现上述目的。既然迪杰斯特拉是用于计算单个源点到各个顶点之间的最短路径,那么只需要对每个顶点都执行一次迪杰斯特拉算法,便可以得到每一对顶点之间的最短路径。迪杰斯特拉的时间复杂度是 O(n²),如果对每个顶点都执行一次,那么时间复杂度将变为 O(n³)。
不过,对迪杰斯特拉算法改造来计算每对顶点之间的最短路径在代码实现上是比较复杂的,这里我们学习一种新的算法,叫做弗洛伊德(Floyd)算法。
弗洛伊德算法在求解每一对顶点之间的最短路径问题的代码实现上相对于迪杰斯特拉算法简单很多,它的时间复杂度也是 O(n³)。
二、弗洛伊德算法的思想与步骤
2.1 基本思想
通常从源点到终点,并不是直接到达的,而是需要经过许多中转站来一步步到达。例如下图,<V4,V5>、<V5,V2> 之间的距离分别为 4 和 5。可以看到,V4 和 V2 之间不能直接通达,如果要计算 V4 和 V2 之间的距离,我们就要借助 V5 来作为中转站,因此 V4 到 V2 的距离为 4+5=9。
如果要计算 V4 和 V3 之间的距离呢?这个时候只借助 V5 就无法完成任务了,还需要额外借助 V2。
因此,计算所有节点之间的最短距离这个大问题可以分为借助 k 个中转站的情况下计算各节点之间的最短路径,其中 0 ≤ k ≤ |V|。
弗洛伊德算法的基本思想如下:
假设求从顶点 vi 到 vj 的最短路径。如果从 vi 到 vj 有边,则从 vi 到 vj 存在一条长度为 dis[i][j]
(dis 为邻接矩阵)的路径,但是该路径不一定是最短路径,还需要再进行 n 次试探。首先考虑路径 <vi,v0,vj> 是否存在(即 <vi,v0> 、<v0,vj>),如果存在,则比较 <vi,vj> 和 <vi,v0,vj> 的路径长度,然后取较小者为 vi 到 vj 的最短路径,且 vi 到 vj 的中间顶点的序号不大于 0。假如在路径上再增加一个一个顶点 v1,也就是说,如果 <vi,…,v1> 和 <v1,…,vj> 分别是当前找到的中间顶点的序号不大于 0 (即中间顶点可能是 v0 或没有)的最短路径,那么<vi,…,v1,…,vj> 就有可能是从 vi 到 vj 的中间顶点的序号不大于 1 的最短路径。将它和已经得到的从 vi 到 vj 中间顶点序号不大于 0 的最短路径相比较,从中选出中间顶点的序号不大于 1 的最短路径之后,再增加一个顶点 v2,继续进行试探。以此类推,直到增加了所有的顶点作为中间节点。
弗洛伊德算法的核心思想总结下来就是:不断增加中转顶点,然后更新每对顶点之间的最短距离。
2.2 步骤图解说明
如下图所示,演示使用弗洛伊德算法求解每一对顶点之间的最短路径。
-
在没有中转点的情况下,各个顶点之间的最短路径距离,用一个二维数组 dis 表示,如下图所示。
-
假设 v1 作为中转点,则各个顶点之间的最短路径距离理论上将会被更新。然而由于 v2 经中转点 v1 到 v3 的距离大于 v2 直接到 v3 的距离(即
dis[v2][v1] + dis[v1][v3] > dis[v2][v3]
),因此本次最短路径距离并未真正发生变化。 -
再假设 v1,v2 作为中转点。此时 v5 可以经 v2 访问到 v1 和 v3,在没有中转的情况下,v5 到 v1 和 v3 的最短距离都是无穷大,因此各个顶点之间的最短路径距离将会被更新。
-
再假设 v1,v2,v3 作为中转点。此时 v1 经 v3 访问 v2 的距离小于直接访问 v2 的距离(即
dis[v1][v3] + dis[v3][v2] < dis[v1][v2]
),因此 v1 到 v2 的最短距离将会被更新;同时由于 v1 需经 v2 中转访问 v5,因此 v1 到 v5 的最短距离也要同步更新。 -
再假设 v1,v2,v3,v4 作为中转点。由于 v4 是一个孤立的中转点因此,因此各个顶点之间的最短路径并未发生真正变化。
-
再假设 v1,v2,v3,v4,v5 作为中转点。由于 v2 经 v5 中转可达 v4 且距离小于
dis[v2][v4]
,因此 v2 到 v4 的最短路径将会更新 ,且 v1,v3 到 v4 的最短路径也会随之更新。 -
至此,所有的点都作为了中转点,算法执行完毕。此时 dis 数组中存放的就是各个顶点之间的最短路径距离。
三、弗洛伊德算法的代码实现
public class FloydAlgorithm {
private static final int N = 65535; // 表示距离无穷大
public static void main(String[] args) {
String[] vertexes = { "v1", "v2", "v3", "v4", "v5"}; // 顶点
int[][] dis = { // 各个顶点之间的初始最短路径
/*v1*//*v2*//*v3*//*v4*//*v5*/
/*v1*/{0, 6, 3, N, N},
/*v2*/{6, 0, 2, N, 5},
/*v3*/{3, 2, 0, N, N},
/*v4*/{N, N, N, 0, 4},
/*v5*/{N, 5, N, 4, 0},
};
floyd(dis); // 调用弗洛伊德算法
/* 输出每一对顶点之间的最短距离 */
for (int i=0; i<dis.length; i++){
for (int j=0; j<dis[i].length; j++){
System.out.printf("<%s,%s> = %d \t",vertexes[i],vertexes[j],dis[i][j]);
}
System.out.println();
}
}
/**
* 弗洛伊德算法求解每一对顶点之间的最短路径距离
* @param dis 初始最短路径距离
*/
public static void floyd(int[][] dis){
for (int k=0; k<dis.length; k++){ // 遍历中转顶点
for (int i=0; i<dis.length; i++){ // 从下标为 i 的顶点出发
for (int j=0; j<dis.length; j++){ // 终点下标为 j
/* 如果经新的中转点后的最短路径距离小于 dis[i][j],就更新 dis[i][j] */
if (dis[i][k] + dis[k][j] < dis[i][j]){
dis[i][j] = dis[i][k] + dis[k][j];
}
}
}
}
}
}
运行结果如下: