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

原创 2016年06月02日 14:08:57

上两篇博客介绍了计算单源最短路径的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一下哦~ 由于本人还在研究《算法导论》,所以这个项目还会持续更新哦~ 大家一起好好学习~
版权声明:本文为博主原创文章,转载请注明原文地址

最短路径问题---Floyd算法详解

前言 Genius only means hard-working all one’s life. Name:Willam Time:2017/3/81、最短路径问题介绍问题解释: 从图中的...
  • qq_35644234
  • qq_35644234
  • 2017年03月11日 17:01
  • 21021

求图中最短路径算法之Floyd-Warshall算法——C++实现

Floyed-Warshall算法使用动态规划的思想来计算图中所有节点对之间的最短路径,其运行时间复杂度为O(V^3),该算法的约束条件为图中不能包含权值之和为负的环,但可以包含权值为负的边。该算法相...
  • lrgdongnan
  • lrgdongnan
  • 2016年06月26日 16:24
  • 1886

Floyd-Warshall算法求解所有结点对的最短路径问题Java实现

其实求解所有结点对之间的最短路径问题完全可以用调用|V|次Bellman-Ford算法或Dijkstra算法来实现,但是那样肯定效率会比较低下。与前面两个算法基于邻接链表不同,本文所要说的Floyd-...
  • john_bian
  • john_bian
  • 2017年07月07日 11:17
  • 517

Poj 1125 Stockbroker Grapevine(Floyd算法求结点对的最短路径问题)

一、Description Stockbrokers are known to overreact to rumours. You have been contracted to develop a...
  • Insert_day
  • Insert_day
  • 2013年07月25日 18:06
  • 556

Floyd-Warshall算法:求结点对的最短路径问题

Floyd-Warshall算法:是解决任意两点间的最短路径的一种算法,可以正确处理有向图或负权的最短路径问题,同时也被用于计算有向图的传递闭包。 原理: Floyd-Warshall算法的原理是...
  • Insert_day
  • Insert_day
  • 2013年07月25日 17:46
  • 862

所有结点对最短路径问题(Floyd-Warshall算法)——算法导论学习笔记(1)

所有结点对问题可以对所有结点都运行一次Dijkstra算法,若采用的是二插堆来实现最小优先队列,那么该算法的时间复杂度是O(VElgV). 但是Dijkstra算法写起来较复杂,这里讲的Floyd-W...
  • u010761652
  • u010761652
  • 2013年06月18日 23:55
  • 2283

算法导论第二十五章-所有结点对的最短路径问题-Cpp代码实现

这一章就实现了一个Floyd算法。 其中路径的表示没有采用书上的前驱表示,而是用每一对结点中开始结点的后继来表示。...
  • waiwai0331
  • waiwai0331
  • 2016年06月24日 12:21
  • 189

Floyd算法求最短路径

  • 2009年01月03日 11:14
  • 3KB
  • 下载

floyd算法,复杂网络最短路径的计算

  • 2013年04月07日 10:55
  • 714B
  • 下载

Floyd算法求任意两点间的最短路径

  • 2012年12月13日 17:28
  • 5KB
  • 下载
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:结点对最短路径之Floyd算法原理详解及实现
举报原因:
原因补充:

(最多只允许输入30个字)