图算法之最短路径总结

在这里插入图片描述

1、Floyd算法

多源最短路问题:求图中任意两点之间的距离

1.1 核心代码:

for(k=1;k<=n;k++)//通过前k号节点中转
	for(i=1;i<=n;i++)
		for(j=1;j<=n;j++)
			e[i][j]=min(e[i][j],e[i][k]+e[k][j]);

1.2 基本思想

最开始只允许经过1号顶点进行中转,接下来只允许1号和2号顶点进行中转……允许经过1~n号所有顶点进行中转,求任意两点之间的最短路径。用一句话概括就是:从i号顶点到j号顶点只经过前k号 节点的最短路径。

1.3 完整代码

//	算法:Floyd-Warshall
//	时间:2021.11.3 22点41分
//  目的:求解多源路最短路径问题(任意两个点之间的最短路径)
#include<stdio.h>
int main()
{
	int e[10][10], k, i, j, n, m, t1, t2, t3;
	int inf = 99999999;//用inf(infinity的缩写)存储一个我们认为的正无穷值
	//读入n和m,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;
	}

	//Floyd-Warshall算法核心语句
	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("%10d", e[i][j]);
		}
		printf("\n");
	}
	return 0;
}

执行结果:
在这里插入图片描述

2、Dijkstra算法(朴素版)

单源最短路径:指定一个点(源点)到其余各个顶点的最短路径

2.1 算法思想

基本思想:每次找到离源点(这里以1号顶点为源点
)最近的一个顶点,然后以该顶点为中心进行扩展,最终得到源点到其余所有点的最短路径。基本步骤如下:

  1. 将所有顶点分为两部分:已知最短路径的顶点集合P和未知最短路径的顶点集合Q。最开始,已知最短路径的顶点集合P中只有源点一个顶点。我们这里用一个book数组来记录哪些点在集合P中,如果book[i]为0则表示这个顶点在集合Q中。
  2. 设置源点s到自己的最短路径为0即dis[s]=0。若存在有源点能直接到达的顶点i,则把dis[i]=e[s][i].同时把所有其他(源点不能直接到达的)顶点的最短路径设为无穷大(这里我们一般把无穷大具体定义为99999999)。
  3. 在集合Q的所有顶点中选择一个离源点s最近的顶点u(即dis[u]最小)加入到集合P。并考察所有以u为起点的边,对每一条边进行松弛操作。(如果有一条从u到v的边,dis[v]=min(dis[v],dis[u]+e[u][v])。
  4. 重复第三步,如果集合P为空,算法结束。最终dis数组中的值就是源点到所有顶点的最短路径。

2.2 完整代码

//	时间:2021.11.3 23点11分
//	算法:Dijkstra算法
//	目的:求解单源最短路径(指定一个点(源点)到其余各个顶点的最短路径)
#include<stdio.h>
int main()
{
	int e[10][10], dis[10], book[10], i, j, n, m, t1, t2, t3, u, v, min;
	int inf = 99999999;//用inf(infinity的缩写)存储一个我们认为的正无穷值
	//读入n和m,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数组初始化
	for (i = 1; i <= n; i++)
		book[i] = 0;
	book[1] = 1;
	
	//Dijkstra算法核心语句
	for (i = 1; i <= n - 1; i++)//一共n个顶点,1号本身已经在p集合里面了,所以
	{						//只需要循环n-1次即可
		//找到离1号顶点最近的顶点
		min = inf;
		for (j = 1; j <= n; j++)
		{
			if (book[j] == 0 && dis[j] < min)
			{
				min = dis[j];
				u = j;
			}
		}
		book[u] = 1;
		for (v = 1; v <= n; v++)
		{
			if (e[u][v] < inf)
			{
				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]);

	getchar(); getchar();
	return 0;
}

执行结果:
在这里插入图片描述

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

3.1 核心代码

for(k=1;k<=n-1;k++)
	for(i=1;i<=m;i++)
//下面这行代码的意思是:看看能否通过u[i]->v[i](权值为w[i])的这条边,使得一号顶点到v[i]号顶点的距离变短。
		dis[v[i]]=min(dis[v[i]],dis[u[i]]+w[i]);

3.2 基本思想

因为最短路径上最多有n-1条边,因此Bellman-Ford算法最多有n-1个阶段。在每一个阶段,我们对每一条边都要进行松弛操作。其实每实施一次松弛操作,就会有一些顶点已经求得最短路,即这些顶点的最短路的“估计值”变为“确定值”。此后这些顶点的最短路的值就会一直保持不变,不再受后续操作的影响(但是,每次还是会判断是否需要松弛,这里浪费了时间,后续会使用队列优化)。在前k个阶段结束后,就已经找出了从源点发出“最多经过k条边”到达各个顶点的最短路。直到进行完n-1个阶段后,便得到了最多经过n-1条边的最短路。

3.3 完整代码

//	时间:2021.11.14  22点08分
//	算法:Bellman-Ford
//	目的:解决负权边得问题
#include<stdio.h>
int main()
{
	int dis[10], bak[10], i, k, n, m, u[10], v[10], w[10], check, flag;
	int inf = 99999999;//用inf存储一个我们认为得正无穷值
	//读入n和m,n表示顶点个数,m表示边的个数
	scanf("%d %d", &n, &m);

	//读入边
	for (i = 1; i <= m; i++)
		scanf("%d %d %d", &u[i], &v[i], &w[i]);

	//初始化dis数组,这里是1号顶点到其余各个顶点得初始路程
	for (i = 1; i <= n; i++)
		dis[i] = inf;
	dis[1] = 0;

	//Bellman-Ford算法核心语句
	for (k = 1; k <= n - 1; k++)
	{
		//将dis数组备份到bak数组中
		for (i = 1; i <= n; i++) bak[i] = dis[i];
		//进行一轮松弛
		for (i = 1; i <= m; i++)
			if (dis[v[i]] > dis[u[i]] + w[i])
				dis[v[i]] = dis[u[i]] + w[i];
		//松弛完毕后检测dis是否有更新
		check = 0;
		for (i = 1; i <= n; i++) if (bak[i] != dis[i]) { check = 1; break; }
		if (check == 0) break;//如果dis数组没有更新,提前退出循环结束算法
	}
	//检测负权回路
	flag = 0;
	for (i = 1; i <= m; i++)
		if (dis[v[i]] > dis[u[i]] + w[i]) flag = 1;

	if (flag == 1) printf("此图含有负权回路");
	else
	{
		//输出最终的结果
		for (i = 1; i <= n; i++)
			printf("%d ", dis[i]);
	}
	getchar(); getchar();
	return 0;
}

3.4 扩展功能:判断图是否含有负权回路

基本原理:
在进行n-1轮松弛之后,如果依然可以继续松弛,那么此图必然存在负权回路。因为如果存在负权回路,那么每多走一次负权回路就可以得到更短的路径。如果一个图没有负权回路,那么最短路径所包含的边最多为n-1条,即进行n-1轮松弛之后最短路径不会再发生变化。如果在n-1轮松弛之后最短路径依然会发生变化,则该图必然存在负权回路。
核心代码:

//Bellman-Ford算法核心语句
for(k=1;k<=n-1;k++)
	for(i=1;i<=m;i++)
		dis[v[i]]=min(dis[v[i]],dis[u[i]]+w[i]);
//检测负权回路
flag=0;
for(i=1;i<=m;i++)
	if(dis[v[i]]>dis[u[i]]+w[i]) flag=1;
if(flag==1) printf("此图含有负权回路");

4、Bellman-Ford算法的队列优化

4.1 优化思路

上面介绍Bellman-Ford算法的时候我们已经了解到松弛n-1轮是最大值,往往不需要n-1轮即可成功。所以每次仅对最短路径发生变化了的点的相邻边执行松弛操作。但是如何知道当前哪些点的最短路程发生了变化呢??我们可以用一个队列来维护这些点。每次选取队首顶点u,对顶点u的所有出边进行松弛操作。例如有一条u->v的边,如果通过u->v这条边使得源点到顶点v的最短路径变短(dis[u]+e[u][v]<dis[v]),且顶点v不在当前的队列中,就将顶点v放入队尾。需要注意的是,这里我们需要一个数组来判重(判断哪些点已经在队列中)。在对顶点u的所有边松弛完毕后,就将顶点v出队。接下来不断从队列中取出新的队首顶点再进行如上操作,直至队列空为止。

4.2 完整代码

//	时间:2021.11.14  22点44分
//	算法:Bellman-Ford 算法队列优化
#include<stdio.h>
int main()
{
	int n, m, i, j, k;
	//u,v和w的数组大小要根据实际情况来设置,要比m的最大值要大一
	int u[8], v[8], w[8];
	//first要比n的最大值大1,next要比m的最大值要大一
	int first[6], next[8];
	int dis[6] = { 0 }, book[6] = { 0 };//book数组用来记录那些顶点已经在队列中了
	int que[101] = { 0 }, head = 1, tail = 1;//定义一个队列,并初始化队列
	int inf = 99999999;//用inf来存储一个我们认为的正无穷值
	//读入n,m
	scanf("%d %d", &n, &m);

	//初始化dis数组,这里是1号顶点到其余各个顶点的初始化路程
	for (i = 1; i <= n; i++)
		dis[i] = inf;
	dis[1] = 0;

	//初始化book数组,初始化为0,刚开始都不在队列中
	for (i = 1; i <= n; i++) book[i] = 0;

	//初始化first数组下标1~n的值为-1,表示1~n顶点暂时都没有边
	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;
	}
	//1号顶点入队
	que[tail] = 1; tail++;
	book[1] = 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];//更新顶点1到顶点v[k]的路程
				//这里的book数组用来判断顶点v[k]是否在队列中
				//如果不使用一个数组来标记的话,判断一个顶点是否在队列中每次都需要
				//从队列的head到tail扫一遍,很浪费时间
				if (book[v[k]] == 0)//0表示不在队列中,将顶点v[k]加入队列中
				{
					//下面两句是入队操作
					que[tail] = v[k];
					tail++;
					book[v[k]] = 1;//同时标记顶点v[k]已经入队
				}
			}
			k = next[k];
		}
		//出队
		book[que[head]] = 0;
		head++;
	}

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

	getchar(); getchar();
}

运行结果:
在这里插入图片描述

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值