求图的最短路径---四中算法优化

最短路径的四种算法!

目录

1、Floyd-Warshall算法 --- 只有五行的算法

2、Dijkstra算法 --- 通过边实现松弛

3、Bellman-Ford --- 解决负权边

4、Bellman-Ford的队列优化

 


1、Floyd-Warshall算法 --- 只有五行的算法

  描述:

求图的最短路径---四中算法优化

在上面的图中,有四个顶点八条边,边有长有短,我们需要求任意两个顶点之间的最短路径。

这个问题也被称为“多源最短路径”问题。

  思路:

首先,我们需要一个矩阵来存储这个图,没错!就是需要邻接矩阵。这里为e[][]。例如顶点1到顶点2的距离为2,那么

e[1][2] == 2 。顶点2没有办法直接到顶点4,那么e[2][4] == inf(正无穷) 。另外,我们规定自己到自己的距离为0 。

                                              求图的最短路径---四中算法优化

虽然深度优先搜索或广度优先搜索可以解决最短路径的问题,可是还有没有别的方法呢??

我们先来看怎么求路径,除了一个顶点直接到另一个顶点之外,还有一种方法就是,从一个顶点先到一个中转点,然后从这个中转点再到另一个点,然后对比一下,这两个方法哪种路径更短,我们就更新路径的值,这下就可以做到使多源路径最短。

举个例子:假如我们现在只能经过一号顶点来判断任意两点之间是否具有最短路径。我们用下面的代码来实现!

for (i = 1; i <= n; i++)
{
	for (j = 1; j <= n; j++)
	{
		if (e[i][j] > e[i][1] + e[1][j])
		{
			e[i][j] > e[i][1] + e[1][j];
		}
	}
}

但是现在还做不到将所有的任意两点之间的路径更新为最短,那么现在只需要加一个循环就行了,那就是循环所有需要经过的顶点。

for (k = 1; k <= n; k++)
{
	for (i = 1; i <= n; i++)
	{
		for (j = 1; j <= n; j++)
		{
			if (e[i][j] > e[i][k] + e[k][j])
			{
				e[i][j] > e[i][k] + e[k][j];
			}
		}
	}
}

这也就是他的名字由来“只有五行代码的算法”。

  源代码

#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>
#include <stdlib.h>

/*
* 在一个有向图中,两个顶点往往不是最短长度,往往需要另一个顶点去过度
  本程序用一个算法实现更新邻接矩阵,实现两顶点之间的最短路径
* 郭文峰
* 2018/10/25
*/

int main(void)
{
	int e[51][51] = { 0 };
	int i = 0;
	int j = 0;
	int k = 0;
	int n = 0;
	int m = 0;
	int inf = 99999999999;
	int t1 = 0;
	int t2 = 0;
	int t3 = 0;

	scanf("%d%d", &n, &m);

	//初始化邻接矩阵
	for (i = 1; i <= n; i++)
	{
		for (j = 1; j <= n; j++)
		{
			if (i == j)
				e[i][j] = 0;
			else
				e[i][j] = inf;
		}
	}

	//输入各个顶点之间的距离
	for (i = 1; i <= m; i++)
	{
		scanf("%d%d%d", &t1, &t2, &t3);
		e[t1][t2] = t3;
	}

	//更新邻接矩阵最短路径
	//分别通过K顶点过度,更新路径
	for (k = 1; k <= n; k++)
	{
		for (i = 1; i <= n; i++)
		{
			for (j = 1; j <= n; j++)
			{
				if (e[i][j] > e[i][k] + e[k][j])
					e[i][j] = e[i][k] + e[k][j];
			}
		}
	}

	for (i = 1; i <= n; i++)
	{
		for (j = 1; j <= n; j++)
		{
			printf("%3d", e[i][j]);
		}
		printf("\n");
	}

	system("pause");
	return 0;
}

  运行结果

                                                             求图的最短路径---四中算法优化

这个方法求最短路径,他的时间复杂度为O(N^3)。但是他只有五行代码,写起来非常的容易,可是有点慢,那么下面就要引入Dijkstra算法。

另外!Floyd-Warshall算法并不能解决有负权回路的问题,例如:

                                        求图的最短路径---四中算法优化

在上面的图中,1->2->3这样的路径每绕一圈最短路径就会减少。

2、Dijkstra算法 --- 通过边实现松弛

  描述:

此算法是用来指定一个点(源点)到其余各个顶点的最短路径,也叫做“单源最短路径”。

例如求下图中的1号顶点到2、3、4、5、6号顶点的最短路径。

  思路:

                                                         求图的最短路径---四中算法优化

与上面的算法一样,还是用一个邻接矩阵来存储这个图。

                                               求图的最短路径---四中算法优化

还需要一个一维数组dis来存储一号顶点到其余各个顶点的初始路程。

                                                    求图的最短路径---四中算法优化

刚开始,我们将dis数组里的值叫做估计值。

首先我们找到离一号顶点最近的顶点,然后循环判断一号顶点直接到该顶点的路径是否大于一号顶点先到其他顶点再到此顶点的路径。

例如:这里离一号顶点最近的是二号顶点,但是二号顶点到一号顶点已经是最短路径了,所以看二号顶点连接着那些顶点。先判断 

if (dis[3] > dis[2] + e[2][3])
    dis[3] = dis[2] + e[2][3];

如果一号顶点离三号顶点的路径大于一号顶点先到二号顶点再到三号顶点,那么就更新路径信息。

以此类推。

  源代码:

#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>
#include <stdlib.h>

/*
* Dijkstra算法,通过边松弛求最短路径
* 郭文峰
* 2018/10/25
*/

int main(void)
{
	int e[51][51] = { 0 };
	int book[51] = { 0 };
	int dis[50] = { 0 };
	int inf = 999999999;
	int i = 0;
	int j = 0;
	int n = 0;
	int m = 0;
	int t1 = 0;
	int t2 = 0;
	int t3 = 0;
	int min = 0;
	int u = 0;
	int v = 0;

	//输入有向图有n个顶点和m条边
	scanf("%d%d", &n, &m);

	//初始化邻接矩阵
	for (i = 1; i <= n; i++)
	{
		for (j = 1; j <= n; j++)
		{
			if (i == j)
				e[i][j] = 0;
			else
				e[i][j] = inf;
		}
	}

	//输入各个顶点之间边的长度
	for (i = 1; i <= m; i++)
	{
		scanf("%d%d%d", &t1, &t2, &t3);
		e[t1][t2] = t3;
	}

	//初始化dis数组
	for (i = 1; i <= n; i++)
		dis[i] = e[1][i];


	book[1] = 1;

	//算法核心部分
	for (i = 1; i <= n - 1; i++)
	{
		//找到离源点最近的一个点
		min = inf;
		for (j = 1; j <= n; j++)
		{
			if (dis[j] < min && book[j] == 0)
			{
				min = dis[j];
				u = j;
			}
		}
		//标记u点
		book[u] = 1;

		//更新最短路径
		for (v = 1; v <= n; v++)
		{
			
			if (dis[v] > dis[u] + e[u][v])
			{
				dis[v] = dis[u] + e[u][v];
			}
			
		}
	}

	for (i = 1; i <= n; i++)
	{
		printf("%d ", dis[i]);
	}

	system("pause");
	return 0;
}

  运行结果:

                                                            求图的最短路径---四中算法优化

上面的代码实现复杂度为O(N^2)。

3、Bellman-Ford --- 解决负权边

  描述:

Dijkstra算法虽然好,然是并不能解决负权边的问题。Bellman-Ford算法无论是思想上还是代码实现上都堪称完美。

  思路:

核心代码就四句,我们先来看看。

for (k = 1; k <= n - 1; k++)
{
	for (i = 1; i <= m; i++)
	{
		if (dis[v[i]] > dis[u[i]] + w[i])
			dis[v[i]] = dis[u[i]] + w[i];
	}
}

上面的代码外循环共循环了n-1次,因为1号顶点不需要和自己比,只需要和别的顶点比较,所以是n-1 。内循环循环了m次,就是枚举每一条边。dis数组的作用于Dijkstra算法一样,用来记录源点到各个顶点之间的距离。u、v、w、数组是用来记录边的信息,就是邻接表。

if (dis[v[i]] > dis[u[i]] + w[i])
	dis[v[i]] = dis[u[i]] + w[i];

上面两个代码的意思是,看看一号顶点到v[i]顶点是否可以通过u[i]进行缩短,这种操作叫做松弛。

如果想把所有边都松弛一遍,只需要加一个循环就行。

for (i = 1; i <= m; i++)
	{
		if (dis[v[i]] > dis[u[i]] + w[i])
			dis[v[i]] = dis[u[i]] + w[i];
	}

 

然后跟Dijkstra算法的思想一样,还是用dis数组来存储所有顶点到一号顶点之间的路径。然后循环n-1次松弛每条边。

  源代码:

#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>
#include <stdlib.h>

/*
* 本程序是BellmanFord算法求最短路径。
  用到了邻接表三个数组分别为u[]、v[]、w[]。
  chenk来判断是否更新结束。
  flag来判断是否有负权值。
* 郭文峰
* 2018/10/26
*/

int main(void)
{
	int dis[10] = { 0 };
	int bak[10] = { 0 };
	int i = 0;
	int j = 0;
	int k = 0;
	int check = 0;
	int flag = 0;
	int n = 0;
	int m = 0;
	int u[10] = { 0 };
	int v[10] = { 0 };
	int w[10] = { 0 };
	int inf = 999999999;

	//输入n个顶点和m条边
	scanf("%d%d", &n, &m);

	//输入数据建立邻接表
	for (i = 1; i <= m; i++)
	{
		scanf("%d%d%d", &u[i], &v[i], &w[i]);

	}

	//初始化dis数组
	for (i = 1; i <= n; i++)
	{
		dis[i] = inf;
	}
	dis[1] = 0;

	for (k = 1; k <= n - 1; k++)
	{
		//先让bak数组等于dis数组
		for (i = 1; i <= n; i++)
		{
			bak[i] = dis[i];
		}

		//更新最短路径
		for (j = 1; j <= m; j++)
		{
			if (dis[v[j]] > dis[u[j]] + w[j])
				dis[v[j]] = dis[u[j]] + w[j];
		}

		check = 0;
		//判断是否更新
		for (i = 1; i <= n; i++)
		{
			if (bak[i] != dis[i])
			{
				check = 1;
				break;
			}
		}
		if (check == 0)
			break;

	}

	//判断是否有负权值
	for (j = 1; j <= m; j++)
	{
		if (dis[v[j]] > dis[u[j]] + w[j])
			flag = 1;
	}

	if (flag == 1)
		printf("有负权值!");
	else
	{
		for (i = 1; i <= n; i++)
		{
			printf("%d ", dis[i]);
		}
	}

	system("pause");
	return 0;
}

  运行结果:

                                                        求图的最短路径---四中算法优化

此算法的时间复杂度小于O(NM)。

4、Bellman-Ford的队列优化

  描述:

Bellman-Ford算法的时间复杂度并不算小,所以我们需要优化它,我们需要每次仅对最短路估计值发生变化了的顶点的所有出边执行松弛操作。我们可以用一个队列来维护他。

  思路:

每次选取队首顶点u,对顶点u的所有出边进行松弛操作。例如有一条u->v的边,如果一条边使得源点到顶点v的最短路程变短,且顶点v不在当前的队列中,就将顶点v放入队尾。还需要一个数组来判重,不能重复出现,在顶点u的所有出边松弛完毕后,就将顶点u出队,接下来不断从队列中取出新的队首顶点再进行如上操作,直到队列为空为止。

 

求图的最短路径---四中算法优化

在这样一个图里,先从一号顶点开始,对一号顶点的出边进行松弛,例如2号顶点松弛成功,就将2号顶点入队……

在一号顶点所有边都松弛完毕后,一号顶点出队,然后到2号顶点,再对2号顶点的所有出边进行松弛。

以此类推。

求图的最短路径---四中算法优化

求图的最短路径---四中算法优化

求图的最短路径---四中算法优化

求图的最短路径---四中算法优化

求图的最短路径---四中算法优化

  源代码:

#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>
#include <stdlib.h>

/*
* 本程序在对BellmanFord算法进行了队列的优化
* 郭文峰
* 2018/10/26
*/

int main(void)
{
	int dis[10] = { 0 };
	int i = 0;
	int k = 0;
	int n = 0;
	int m = 0;
	int head = 1;
	int tail = 1;
	int que[101] = { 0 };
	int first[10] = { 0 };
	int next[10] = { 0 };
	int u[10] = { 0 };
	int v[10] = { 0 };
	int w[10] = { 0 };
	int book[10] = { 0 };
	int inf = 99999;


	scanf("%d%d", &n, &m);

	for (i = 1; i <= n; i++)
	{
		dis[i] = inf;
	}
	dis[1] = 0;

	//初始化first数组
	for (i = 1; i <= n; i++)
	{
		first[i] = -1;
	}

	for (i = 1; i <= m; i++)
	{
		scanf("%d%d%d", &u[i], &v[i], &w[i]);
		//建立邻接表
		next[i] = first[u[i]];
		first[u[i]] = i;
	}

	//将一号顶点入队
	que[tail] = 1;
	tail++;
	book[1] = 1;

	while (head < tail)
	{
		k = first[que[head]];//当前需要处理的队首顶点
		//扫描当前顶点的所有边
		while (k != -1)
		{
			if (dis[v[k]] > dis[u[k]] + w[k])//判断边是否符合松弛的条件
			{
				dis[v[k]] = dis[u[k]] + w[k];//更新

				if (book[v[k]] == 0)
				{
					que[tail] = v[k];
					tail++;
					book[v[k]] = 1;

				}
			}
			k = next[k];
		}
		book[que[head]] = 0;
		head++;
	}

	//输出一号顶点到其余各顶点的最短路径
	for (i = 1; i <= n; i++)
	{
		printf("%d ", dis[i]);
	}

	system("pause");
	return 0;
}





  运行结果:

                                                             求图的最短路径---四中算法优化