Floyd算法又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,与Dijkstra算法类似。该算法是一种在具有正或负边缘权重(但没有负环)的加权图中找到最短路径的算法,即支持负权值但不支持负权环。弗洛伊德算法采用的是动态规划思想,其状态转移方程如下:
其中matrix[i,j]表示i到j的最短距离,k是穷举i到j之间可能经过的中间点,当中间点为k时,对整个矩阵即从i到j的路径长度进行更新,对所有可能经过的中间点进行遍历以得到全局最优的最短路径。算法的单个执行将找到所有顶点对之间的最短路径长度,与迪杰斯特阿拉算法的计算目标有一些差异,迪杰斯特拉计算的是单源最短路径,而弗洛伊德计算的是多源最短路径,其时间复杂度为O(n³)。虽然它不返回路径本身的细节,但是可以通过对算法的简单修改来重建路径,我们利用这个思想,通过递归的方式访问每条路径经过的中间节点,对最终的路径进行输出。
算法描述
在有向图 G=(V,E) 中,假设每条边 E[i] 的长度为 w[i],找到V钟任意两点之间的路径长度最小值。
算法流程
本节将对算法流程进行模拟,设置Graph为包含7个顶点和9条边的有向无环图,Graph如下:
弗洛伊德算法选取某个节点k作为i到j需要经过的中间节点,通过比较d(i,k)+d(k,j)和现有d(i,j)的大小,将较小值更新为路径长度,对k节点的选取进行遍历,以得到在经过所有节点时i到j的最短路径长度,通过不断加入中间点的方式更新最短路径。同时在path数组中存储i到j所经过的中间节点k,用于最后递归调用输出路径结果。
算法步骤如下:
1、初始化矩阵。
2、选取0号节点作为中间点,初始化矩阵。
3、选取1号节点作为中间点,更新矩阵,通过两层循环计算(i->1),(1->j)的路径是否比目前i到j的路径长度更短。此时可以将矩阵数值看作是将0、1作为中间点获得的多源最短路径长度。
4、选取2号节点作为中间点,更新矩阵,通过两层循环计算(i->2),(2->j)的路径是否比目前i到j的路径长度更短。此时可以将矩阵数值看作是将0、1、2作为中间点获得的多源最短路径长度。
5、选取3号节点作为中间点,更新矩阵,通过两层循环计算(i->3),(1->3)的路径是否比目前i到j的路径长度更短。此时可以将矩阵数值看作是将0、1、2、3为中间点获得的多源最短路径长度。
6、选取4号节点作为中间点,更新矩阵,通过两层循环计算(i->4),(4->j)的路径是否比目前i到j的路径长度更短。此时可以将矩阵数值看作是将0、1、2、3、4作为中间点获得的多源最短路径长度。
7、选取5号节点作为中间点,更新矩阵,通过两层循环计算(i->5),(5->j)的路径是否比目前i到j的路径长度更短。此时可以将矩阵数值看作是将0、1、2、3、4、5作为中间点获得的多源最短路径长度。
8、选取6号节点作为中间点,更新矩阵,通过两层循环计算(i->6),(6->j)的路径是否比目前i到j的路径长度更短。此时可以将矩阵数值看作是将所有节点作为中间点获得的多源最短路径长度,遍历结束,得到最后结果。
算法实现
public class FloydAlgorithm {
public static int MaxValue = 100000;
public static int[][] path;
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.println("请输入顶点数和边数:");
//顶点数
int vertex = input.nextInt();
//边数
int edge = input.nextInt();
int[][] matrix = new int[vertex][vertex];
//初始化邻接矩阵
for (int i = 0; i < vertex; i++) {
for (int j = 0; j < vertex; j++) {
matrix[i][j] = MaxValue;
}
}
//初始化路径数组
path = new int[matrix.length][matrix.length];
//初始化边权值
for (int i = 0; i < edge; i++) {
System.out.println("请输入第" + (i + 1) + "条边与其权值:");
int source = input.nextInt();
int target = input.nextInt();
int weight = input.nextInt();
matrix[source][target] = weight;
}
//调用算法计算最短路径
floyd(matrix);
}
//非递归实现
public static void floyd(int[][] matrix) {
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix.length; j++) {
path[i][j] = -1;
}
}
for (int m = 0; m < matrix.length; m++) {
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix.length; j++) {
if (matrix[i][m] + matrix[m][j] < matrix[i][j]) {
matrix[i][j] = matrix[i][m] + matrix[m][j];
//记录经由哪个点到达
path[i][j] = m;
}
}
}
}
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix.length; j++) {
if (i != j) {
if (matrix[i][j] == MaxValue) {
System.out.println(i + "到" + j + "不可达");
} else {
System.out.print(i + "到" + j + "的最短路径长度是:" + matrix[i][j]);
System.out.print("最短路径为:" + i + "->");
findPath(i, j);
System.out.println(j);
}
}
}
}
}
//递归寻找路径
public static void findPath(int i, int j) {
int m = path[i][j];
if (m == -1) {
return;
}
findPath(i, m);
System.out.print(m + "->");
findPath(m, j);
}
}
复制
样例输入:
7 10 0 1 6 1 2 5 0 3 2 3 1 7 3 4 5 1 2 5 1 5 3 5 2 3 5 4 2 4 6 1
Q&A
问:算法中的三层for循环,每一层分别控制什么?
答:第一层循环设置中间点k,第二层循环设置起始点i,第三层循环设置结束点j。
问:为什么弗洛伊德算法支持负权值?
答:因为路径更新是根据新值和旧值比较获得的,最终的结果都是在最后一次迭代过程中对全局进行更新而得到的,中间的每次迭代只是一次局部调整而非最终结果。而不像迪杰斯特拉采用的贪心策略,每一次迭代都确定出一条最短路径,负权的出现使得不能保证每次迭代都是最优解。