结点对最短路径之Floyd算法原理详解及实现

版权声明:本文为博主原创文章,转载请注明原文地址 https://blog.csdn.net/Ivan_zgj/article/details/51566336

上两篇博客介绍了计算单源最短路径的Bellman-Ford算法和Dijkstra算法。Bellman-Ford算法适用于任何有向图,即使图中包含负环路,它还能报告此问题。Dijkstra算法运行速度比Bellman-Ford算法要快,但是其要求图中不能包含负权重的边。

单源最短路径之Bellman-Ford算法

单源最短路径之Dijkstra算法

在很多实际问题中,我们需要计算图中所有结点对间的最短路径。当然,我们可以使用上述两种算法来计算每一个顶点的单源最短路径,对于图G=(V,E)来说,使用Bellman-Ford算法计算结点对最短路径的时间复杂度为O(V^2 * E),使用Dijkstra算法计算结点对最短路径的时间复杂度为O(V^3)。本文将会介绍一种应用更广泛的算法,而且它可以应用于有负权重边但没有负环路的图中,其时间复杂度为O(V^3),那就是Floyd-Warshall算法。

1. Floyd算法的原理

在上一篇博客 单源最短路径之Dijkstra算法 中,提到了图的一个重要性质:一条最短路径的子路径也是一条最短路径。因此,一条最短路径要么只包含一条直接相连的边,要么就经过一条或多条到达其它顶点的最短路径。

上图给出的是顶点i到顶点j的路径示意图。i到j的路径为<i,...,k,...,j>,其中顶点k是路径i到j的一个编号最大的中间顶点,即路径<i,...,k>中的所有顶点编号求取自集合{1,2,3,...,k-1},路径<k,...,j>也是一样的。因为路径<i,...,k,...,j>为最短路径,那么路径<i,...,k>路径<k,...,j>也是最短路径。对路径<i,...,k>路径<k,...,j>也可以递归做出上述操作。

于是,我们可以推出如下递归公式。

dij(k) = wij                                                        当k=0;

dij(k) = min(dij(k-1), dik(k-1)+dkj(k-1))             当k>0;

上述公式中dij为顶点i到顶点j的当前路径的长度,k是当前递归中路径的最大顶点编号。当k=0时,路径的中间顶点的编号不大于0,即不存在任何中间顶点,这种情况顶点i到顶点j的路径必然只是一条连接这两个顶点的边,因此其长度为该边的权重。当k>0,每次递归时加入编号为k的顶点,可以根据其它"当前最短路径"构造顶点i到顶点j的一条新路径,并与其原路径进行比较,从中选择更短的。这是一种自底向上的动态规划算法。

2. Floyd算法的C实现

本文实现的Floyd算法所需要的输入与前面的博客介绍的不一样。前面介绍的所有图算法需要的图都是用邻接表表示的。下面给出的Floyd算法需要的图使用邻接矩阵表示的,即权重图。该实现使用前驱子图(二维矩阵)来记录结点对的最短路径的目的顶点的前驱顶点编号(前一个顶点的编号)。

/**
* Floyd 寻找结点对的最短路径算法
* w 权重图
* vertexNum 顶点个数
* lenMatrix 计算结果的最短路径长度存储矩阵(二维)
* priorMatrix 前驱子图(二维),路径<i, ..., j>重点j的前一个顶点k存储在priorMatrix[i][j]中
*/
void Floyd_WallShall(int **w, int vertexNum, int **lenMatrix, int **priorMatrix)
{
	// 初始化
	for (int i = 0; i < vertexNum; i++)
	{
		for (int j = 0; j < vertexNum; j++)
		{
			*((int*)lenMatrix + i*vertexNum + j) = *((int*)w + i*vertexNum + j);
			if (*((int*)w + i*vertexNum + j) != INF && i != j)
			{
				*((int*)priorMatrix + i*vertexNum + j) = i;
			}
			else
			{
				*((int*)priorMatrix + i*vertexNum + j) = -1;
			}
		}
	}

	// Floyd算法
	for (int k = 0; k < vertexNum; k++)
	{
		for (int i = 0; i < vertexNum; i++)
		{
			for (int j = 0; j < vertexNum; j++)
			{
				int Dij = *((int*)lenMatrix + i*vertexNum + j);
				int Dik = *((int*)lenMatrix + i*vertexNum + k);
				int Dkj = *((int*)lenMatrix + k*vertexNum + j);
				if (Dik != INF && Dkj != INF && Dij > Dik + Dkj)
				{
					*((int*)lenMatrix + i*vertexNum + j) = Dik + Dkj;
					*((int*)priorMatrix + i*vertexNum + j) = *((int*)priorMatrix + k*vertexNum + j);
				}
			}
		}
	}
}
上述程序需要输入一个邻接矩阵,顶点的个数,以及用于存储结果路径长度的矩阵和前驱子图矩阵。这些矩阵本质上均是一个二维数组。该算法首先对长度矩阵和前驱子图进行初始化,也就是递推公式当k=0时的操作,然后就进入循环反复更新结点对的路径。算法没计算一次所有结点对的路径,需要进行V^2次运算,而算法需要从小到大依次将V个顶点加入到图中进行运算,于是整个算法的时间复杂度为O(V^3)。

这里简单说一下前驱子图priorMatrix。我们可以通过前驱子图找到任意结点对的最短路径。例如我们要找到顶点i到顶点j的一条最短路径,可以先找到k=priorMatrix[i][j],此时就知道路径为<i,...,k,j>,然后我们再找到路径<i,...,k>的前驱顶点,即priorMatrix[i][k],如此类推。这一操作的正确性由上面提到的性质(一条最短路径的子路径也是一条最短路径)保证。

下面给出一个应用上述算法的例子。

	int w[5][5] = {	0,		3,		8,		INF,	-4,
					INF,	0,		INF,	1,		7,
					INF,	4,		0,		INF,	INF,
					2,		INF,	-5,		0,		INF,
					INF,	INF,	INF,	6,		0};
	int lenMatrix[5][5];
	int priorMatrix[5][5];

	Floyd_WallShall((int**)w, 5, (int**)lenMatrix, (int**)priorMatrix);
	for (int i = 0; i < 5; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			if (lenMatrix[i][j] == INF)
			{
				printf("从%d到%d\t\t长度:INF\n", i, j);
			}
			else
			{
				printf("从%d到%d\t\t长度:%d\t\t路径:", i, j, lenMatrix[i][j]);
				printIJPath((int**)priorMatrix, 5, i + 1, j + 1);
			}
		}
	}
printIJPath方法的定义如下。

/**
* 根据前驱子图打印i到j的路径,输入顶点编号从1开始,输出顶点编号从1开始
*/
void printIJPath(int **prior, int vertexNum, int i, int j)
{
	i--; j--;
	printf("%d", j + 1);
	int k = *((int*)prior + i*vertexNum + j);
	while (k != -1)
	{
		printf(" <- %d", k + 1);
		k = *((int*)prior + i*vertexNum + k);
	}
	printf("\n");
}
上述例程构造的图以及运行结果如下图所示。前驱子图总priorMatrix[i][i]=-1。

长度矩阵
0 1 -3 2 -4
3 0 -4 1 -1
7 4 0 5 3
2 -1 -5 0 -2
8 5 1 6 0
前驱子图
-1 2 3 4 0
3 -1 3 1 0
3 2 -1 1 0
3 2 3 -1 0
3 2 3 4 -

3. 总结

Floyd算法的时间复杂度为O(V^3),因为其实现代码很紧凑,所以时间复杂度的常数项很小。Floyd算法是一种应用非常广泛的计算结点对最短路径的算法。其实还有一种结合了Bellman-Ford算法和Dijkstra算法的Johnson算法,该算法在用于稀疏图时运行速度比Floyd算法更快,并且能够报告图中存在负环路的情况(得益于Bellman-Ford算法)。Johnson算法的时间复杂度为Bellman-Ford算法的时间复杂度加上Dijkstra算法的时间复杂度。如果使用二叉堆实现Dijkstra算法的最小优先队列,那么Johnson算法时间复杂度为O(VElgV+VE)=O(VElgV)。Johnson算法的具体介绍可以参考其它资料,下面给出的个github项目中也有具体的C实现代码。

完整的程序可以看到我的github项目 数据结构与算法

这个项目里面有本博客介绍过的和没有介绍的以及将要介绍的《算法导论》中部分主要的数据结构和算法的C实现,有兴趣的可以fork或者star一下哦~ 由于本人还在研究《算法导论》,所以这个项目还会持续更新哦~ 大家一起好好学习~
阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页