图论——Floyd-Warshall算法

前言:

我学习弗洛伊德算法的起因是遇到了这样一道关于最短路径的问题:
图论——Floyd-Warshall算法_第1张图片
在此之前,有关图论最短路径的算法,我只学习过深度优先搜索和广度优先搜索,而这两种算法获取最短路径的过程,无非都是将全部的从起点到终点的可能路径完全搜索出来,然后从中不断挑选更新最短的路径,这样的方法在小规模的图中是完全适用的,但是在上面这样的题中,如果我们使用这两种方法将会发生什么?其结果是令人难堪的,因为根据题意,搜索的结果将等价于用先序或者层序的方式去完全展开一颗十分巨大的树,这棵树将会有近乎100层之高,每一个节点的搜索范围是距离其+21到-21的全部节点,近似为40个节点的话,那么将是40的100次方之多,然而我们还要减去重复搜索当前路径上节点的情况,即便如此,也不改变它是近似于某一个常数的100次方的事实,就那一颗规模远小于此的二叉树来讲,然而2的100次方已经是一个无法想象的规模了,其值约等于1.27e30,假设计算机每秒能执行1e9条语句,即便这样也需要1.27e21秒,这个时间长得令人窒息,因为它将等于4e13多年…所以这使我不得不放弃广度优先和深度优先搜索,而另寻它径。

Floyd算法:

Floyd算法其实可以看作是一类动态规划,假设一张图中有n个节点,其中节点之间以无向的有权边相连,那么任意两个节点i和j中可能有m个中间节点k,也就是我们可以先通过i节点到达k节点,然后再通过k节点到达i节点,如果这样的节点存在,那么它就是i和j的中间节点。当然为了从i到达j,我们也可以经过不只一个这样的中间节点k,只要我们能获得从i到达j的最短权值路径即可。
Floyd算法获得最短路径的思路是,我们确保每一个可以到达的节点k,它与i之间的路径都是最短路径,那么我们只需要再找到一条从j到k的最短路径,那么这两条最短路径的和就是从i到j的最短路径。
假设我们现在已经计算出了从1到第100个节点的最短路径,并且将这些最短路径存了起来,那么如果我们现在要到达第101个节点并获得其最短路径的话,那么这时第101个节点就是前面的节点j而前100个节点都可以作为中间节点k,如果它们都可以从1到达的话,在我们计算1到101的最短路径时,我们就逐个比较,看看目前从1到101的最短路径有没有从1到k,再从k到j的短,如果没有那我们就将最短路径更新为这条新的路径的长度,这样做的依据是,由于之前的计算,对于每一个从i到k的路径距离d(i,k)和每一个从k到j的距离d(k,j)都已经是最短的了,我们只需要从中挑选一个i,k,j的组合使其进一步达到最短即可,当然这里i和j在最后的目的上是确定的。

这里我们再通过Floyd算法来解决一开始的那个最短路径的问题,如果不加优化地套用Floyd算法的一般模板的话,代码如下:

#include
int d[2022][2022];
int gcd(int a, int b) {
      return b == 0 ? a : gcd(b, a % b); }
int lcm(int a, int b) {
      return a / gcd(a, b) * b; }
Init_map()
{
     
	
	int i,j;
	for(i=1;i<=2021;i++)
	for(j=1;j<=2021;j++)
	d[i][j]=999999999;
	for(i=1;i<=2021;i++)
	{
     
		for(j=i-21>=1?i-21:1;j<=2021&&j<=i+21;j++)
		{
     
			d[i][j]=lcm(i,j);
		}
	}
}
Floyd()
{
     
	Init_map();
	int i,j,k;
	for(i=1;i<=2021;i++)
	{
     
		for(j=1;j<=2021;j++)
		{
     
			for(k=1;k<=2021;k++)
			{
     
				if(d[i][j]>d[i][k]+d[k][j])
				{
     
					d[i][j]=d[i][k]+d[k][j];
				}
			}
		}
	}
}
main()
{
     
	Floyd();
	printf("min_step=%d",d[1][2021]);
}

图论——Floyd-Warshall算法_第2张图片
这的确是正确的运行结果,并且程序在40s左右计算完成了,但是这是十分笨拙的算法,其中做了大量重复而愚蠢的动作,它将其实源节点i,中间节点k,终点j都做了问题规模N的循环,最终把时间复杂度拖到了N^3以上。

但是这个问题经过分析之后其实完全可以进行优化以大大减少其时间复杂度,我们先给出优化后的代码,以及其优化后的运行时间:
优化后的代码如下:

#include
int d[2022][2022];
int gcd(int a, int b) {
      return b == 0 ? a : gcd(b, a % b); }
int lcm(int a, int b) {
      return a / gcd(a, b) * b; }
Init_map()
{
     
	
	int i,j;
	for(i=1;i<=2021;i++)
	for(j=i;j<=2021;j++)
	d[i][j]=999999999;
	for(i=1;i<=2021;i++)
	{
     
		for(j=i-21>=i?i-21:i;j<=2021&&j<=i+21;j++)
		{
     
			d[i][j]=lcm(i,j);
		}
	}
}
Floyd()
{
     
	Init_map();
	int j,k;
		for(j=1;j<=2021;j++)
		{
     
			for(k=1;k<=j;k++)
			{
     
				if(d[1][j]>d[1][k]+d[k][j])
				{
     
					d[1][j]=d[1][k]+d[k][j];
				}
			}
		}
}
main()
{
     
	Floyd();
	printf("min_step=%d",d[1][2021]);
}

图论——Floyd-Warshall算法_第3张图片
这次只用了0.1s多就解决了问题,又比之前快了几百倍!
这得益于对问题的分析和简化:
关于这道多源最短路径的问题,我们对其Floyd做如下分析:
首先将每个节点的邻接关系定义在一个2022*2022的邻接矩阵d[2022][2022]中,其中索引为1的位置开始存元素。我们首先需要初始化节点之间的有权边的值,依据题意,只有在当前节点+21到-21的节点范围内才可以到达,超出这个节点的节点无法被到达,所以我们将这些不能到达的节点的边的权赋值为一个无穷大的值以表示其无法到达,这里用一个很大的数来代替这个无穷大的概念。而在可以到达的节点中,将边w(i,j)的值赋值为i和j 的最小公倍数即可,这里需要注意最小公倍数求解的算法的效率,这将影响整个Floyd算法的效率。

在将整个地图节点之间的边的权值初始化完毕后,我们就要开始使用Floyd算法来动态地求解从1到2021节点的最小有权路径,其思路依然是不断找到可以使路径缩短的中间节点k,来获得这条最短路径。
我们要找的目标使一条从1节点出发到达2021节点的最短路径,那么我们首先就要考虑从1节点可以到达的前22个节点中,对于每一个要到达的节点j,是否存在中间节点k,可以使1->k->j这样的路径比1->j的路径短,其判断条件在代码中等价于:
if(d[1][j]>d[1][k]+d[k][j])
如果这样的路径存在,那么我们将d[1][j]的值更新为d[1][k]+d[k][j]
这就代表我们找到了目前为止的从1到j的最短路径,这样将前22个可到达的节点全部计算完毕后我们就获取了从1出发到前22个几点任意一个节点的已知最短路径,但是此时我们还没有到达我们的目标节点2021,并且还差得很远,所以我们将j的值继续增大,来获得从1到达后面节点的最短路径。
注意:
1.我们在这道问题中其实只需要里利用邻接矩阵的上三角矩阵即可,因为其实对于j>i的情形,d[j][i]是没有意义的,因为为了获得最小路径,我们是不会走回头路的,而d[j][j]其实j>i那么就表示从更远的节点到更近的节点的距离,这样的数据在这道题中是不需要的,所以其不具有存在的意义。那么我们就不对它的下三角矩阵做处理
2.对于源节点i,我们在这里不用将它进行循环,只需要让它始终等于1即可,因为我们最终是为了获得从1节点到2021节点的最短距离,那么我们只需要不断循环j节点来向后延申终点,并通过查找可以缩短距离的中间节点k来获得从i到j的最短路径即可,一开始23节点及其以后的节点是无法到达的,但是当我们对j的22次循环结束后,我们已经可以从1到达前22个节点,那么再当j循环到23及以后节点时,此时d[1][23]显然是无穷大,但是我们通过k节点,也就是前22个已经到达的节点就可以进一步到达23节点,因为d[1][k]+d[k][23]的值不是无穷大的,前者是我们刚才已经计算出的最短路径,后者是初始化权值边得到的结果,我们就可以用这个值来代替d[1][23]从而表示到达了23节点

这样当j最终循环到2021时,我们也就获得了最终的最短路径!
而这样的算法其实更像是一个简单而普通的动态规划,比Floyd算法的一般时间复杂的少很多!其主算法的时间复杂度只有O(N^2)。

从这道题中其实我收获了很多,从一开始无脑地用深搜广搜那样无穷大的时间复杂的,到后来套学习的Floyd模板算法的40s左右的运行时间,再到最终优化版本简单动态规划的0.1几秒。可见算法的优化给程序运行节省的时间是非常可观的!所以对问题的分析和简化就至关重要!

你可能感兴趣的:(数据结构,动态规划,图论,算法)