求最短路径的3种基本方法

求最短路径的3种基本方法

情景简述:n个城市,m条道路,已知每条道路的长度。
总体思路就是:借助中间中间城市 来使得该城市相连的两个城市的路径变短。(就好比A要找C办事,但A不认识C,处理事情就相对麻烦。但A认识B, B认识C,A通过B可以轻易做成想要C做成的事情,从而减少麻烦)。

if (e[i][j] > e[i][k] + e[k][j])
{// A-i    B-k    C-j
	e[i][j] = e[i][k] + e[k][j];
}

1.多源最短路:Floyd-Warshall

算法1.多源最短路:n个城市,m条道路,已知每条道路的长度, 求每两个个城市的最短路程。
思路:每次只允许通过 城市k(中间城市)来获得任意两城市之间的 更短路程-----最开始只允许经过1号城市进行中转,接下来允许1号和2号城市中转………允许1号-n号所有城市进行中转,从而求得任意两个城市之间的最短路程。
例如k = 1 : 即仅允许通过城市1 来缩短其他城市之间的路程。

for (int i = 1; i <= n; ++i)
{
    for (int j = 1; j <= n; ++j)
    {
    	if (e[i][j] > e[i][1] + e[1][j])
   		{// A-i    B-1    C-j
   			e[i][j] = e[i][1] + e[1][j];
   		}
   	}
}

完整代码实现为:

#define _CRT_SECURE_NO_WARNINGS
#include <cstdio>
const int maxn = 1024;
const int inf = 0x3f3f3f3f;
int e[maxn][maxn];

void Init(int n)
{
	for (int i = 1; i <= n; ++i)
	{
		for (int j = 1; j <= n; ++j)
		{
			e[i][j] = inf;
		}
	}
}
void Floyd(int n)
{
	for (int k = 1; k <= n; ++k)
	{
		for (int i = 1; i <= n; ++i)
		{
			for (int 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 (int i = 1; i <= n; ++i)
	{
		for (int j = 1; j <= n; ++j)
		{
			if (i == j)	printf(j == 1 ? "0" : " 0");
			else 		printf(j == 1 ? "%d" : " %d", e[i][j]);
		}
		putchar('\n');
	}
}

int main()
{
	int n, m;
	scanf("%d %d", &n, &m);		// n个结点,m条边
	
	Init(n);					// 初始化e数组 
	for (int i = 1; i <= m; ++i)
	{
		int x, y, d;
		scanf("%d %d %d", &x, &y, &d);
		e[x][y] = d;
	}
	Floyd(n);					//floyd算法
	
	return 0;
}

给出一组测试数据:
4 8
1 2 2
1 3 6
1 4 4
2 3 3
3 1 7
3 4 1
4 1 5
4 3 12
测试结果:
0 2 5 4
9 0 3 4
6 8 0 1
5 7 10 0

2.单源最短路:Dijkstra算法

2.单源最短路:n个城市,m条道路,已知每条道路的长度, 求所给城市到其他多有城市的最短路程。
思路:每次找距离源点(A)最近的城市(B),以该城市作为中间城市进行扩展,用distance数组存储路程,book数组标记此次的中间城市是否被扩展了,该次的城市扩展完后标记,下次只在未标记的城市中找距离源点(A)最近的城市(B’, B’’, B’’’……)进行扩展,最终得到源点城市到所有城市的最短路径。

查找距离源点(A)最近的城市(B):时间复杂度O(n);
需要进行n次查找(B) : O(n);
时间复杂度:O(n^2);
完整代码实现为

#define _CRT_SECURE_NO_WARNINGS
#include <cstdio>
const int maxn = 1024;
const int inf = 0x3f3f3f3f;
int source, e[maxn][maxn], dis[maxn];
bool book[maxn];

void Init(int n)
{
	for (int i = 1; i <= n; ++i)
	{
		for (int j = 1; j <= n; ++j)
		{
			e[i][j] = inf;
		}
	}
}
void Distance(int n)
{
	for (int i = 1; i <= n; ++i)
	{
		dis[i] = e[source][i];
	}	
}
void Dijkstra(int n) 
{
	for (int i = 1; i <= n; ++i)
	{// dijkstra
		int mi = inf, t = -1;
		for (int j = 1; j <= n; ++j)
		{// 找距离源点 最小的 t 
			if (e[source][j] < mi && !book[j])
			{
				mi = e[source][j];
				t = j;
			}
		}
		if (t != -1)	book[t] = 1;
		
		for (int j = 1; j <= n; ++j)
		{// 更新所有的点 
			if ( e[t][j] != inf && e[source][j] > e[source][t] + e[t][j])
			{
				e[source][j] = mi + e[t][j];
			}			
		}	
	}
	
	for (int i = 1; i <= n; ++i)
	{
		if (i == source)	printf(i == 1 ? "0" : " 0");
		else 				printf(i == 1 ? "%d" : " %d", e[source][i]);
	}
}

int main()
{
	int n, m;
	scanf("%d %d", &n, &m);
	scanf("%d",&source);
	book[source] = 1;
	
	Init(n);					//初始化e 
	for (int i = 0; i < m; ++i)
	{
		int x, y, d;
		scanf("%d %d %d", &x, &y, &d);
		e[x][y] = d;			//有向图 
	}
	Distance(n);				//初始化dis 
	Dijkstra(n);				//dijkstra算法 
	
	return 0;
}

注: 若源点已经确定,可把 scanf("%d",&source); 该行代码注释掉, 在全局声明source为已知源点。

3.1解决负权边:Bellman-Ford算法

3.1带负权-单源最短路:n个城市,m条道路,已知每条道路的长度, 求源点到其他所有城市的最短路程。(计算从1号节点出发到其他任意一个节点的最短距离)
在这里插入图片描述在这里插入图片描述思路:用数组u,v来存储结点,对应的权值为w,每次按边进行松弛
即:

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

第一轮就是节点1 只能经过一条边 可以到达其他结点的最短路径(接着就是2-3…n-1条边) ,第二轮dis[u[i]] 就存在不是inf的距离了,依次进行(n-1)轮,即可以得到最短路径。(注:进行n-1轮松弛->若还有正权的节点,不是最短路径。->若还有负权节点,无最短路径 )。
在这里插入图片描述 图片来源:https://blog.csdn.net/day_and_night_2017/article/details/96317791

完整代码实现为:

#define _CRT_SECURE_NO_WARNINGS
#include <cstdio>
const int maxn = 1024;
const int inf = 0x3f3f3f3f;
int dis[maxn], u[maxn], v[maxn], w[maxn];
// 结点存在u、v中, 对应的权值为w[i]

int main()
{
	int n, m;
	scanf("%d %d", &n, &m);		// n个顶点,m条边 
	for (int i = 0; i < m; ++i)
	{
		scanf("%d %d %d", u+i, v+i, w+i);
	}
	
	
	for (int i = 1; i <= n; ++i) 
	{							// 初始化dis数组 
		dis[i] = inf;
	}
	dis[1] = 0;					// 从结点1开始 
	
	// Bellman_Ford算法 
	bool check = 1;				// 标记本轮数组dis是否变化 
	for (int k = 1; k < n && check; ++k)
	{// 进行n-1轮松弛->若还有正权的节点,不是最短路径。->若还有负权节点,无最短路径 
		
		check = 0;
		for (int i = 0; i < m; ++i)
		{// 枚举对m条边 和输入对应  下面为v(是可能到达) -> u(已经到达)
			if (dis[v[i]] > dis[u[i]] + w[i])
			{
				dis[v[i]] = dis[u[i]] + w[i];
				check = 1;
			}
		}
		
	 // 第一轮就是节点1 只能经过一条边 可以到达其他结点的最短路径(接着就是2-3...n-1条边)
	}
	
	
	bool flag = 0;				//检验是否有负权回路 
	for (int i = 1; i <= m; ++i) 
	{
		if (dis[v[i]] > dis[u[i]] + w[i])
		{
			flag = 1;
			printf("存在负权回路\n");
		}
	}
		
	
	for (int i = 1; i <= n; ++i) 
	{
		printf(i == 1 ? "%d" : " %d", dis[i]);
	}
	return 0;
}

测试数据:
5 5
2 3 2
1 2 -3
1 5 5
4 5 2
3 4 3
测试结果:
0 -3 -1 2 4

3.2Bellman-Ford的队列优化

3.2Bellman-Ford的队列优化 -实质是一个广度优先搜索:在实施每一次的松弛操作后, 会有一些顶点已经求得其最短路,此后这些顶点的dis会保持不变,不受后续松弛操作的影响,但是方法3.1还是每次都判断是否需要松弛,耗费了时间。所以每次仅对dis值发生变化的顶点进行松弛操作即可。
该小节会用到邻接表的知识: first数组存储的就是 每个顶点i (u[i]) 的按输入顺序的最后u-v边
eg:
4 5
1 4 9
4 3 8
1 2 5
2 4 6
1 3 7
在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

	for (int i = 1; i <= m; ++i)
	{// 创建邻接表 
		scanf("%d %d %d", u+i, v+i, w+i);
		next[i] = first[u[i]];
		first[u[i]] = i;
	}
// 使用邻接表时:就看k = next[k], k 是否为-1来确定是否结束 eg:5->3->1->-1;

图片来源:https://blog.csdn.net/zbq_tt5/article/details/89681404(不是很清楚的可以在学习一下邻接表存储)

思路:有些结点在实施若干松弛之后,这些点的最短路的估计值就不会再变,每次判断浪费时间,所以每次仅对dis值发生变化的顶点进行松弛操作即可。

#define _CRT_SECURE_NO_WARNINGS
#include <cstdio>
#include <queue>
using namespace std;
const int inf = 0x3f3f3f3f; 
const int maxn = 1024;

int dis[maxn],u[maxn], v[maxn], w[maxn];
int first[maxn], next[maxn];
bool book[maxn];
queue<int>que;


int main()
{
	int n, m, q, p;
	scanf("%d %d", &n, &m);
	
	// 初始化dis数组 		// 初始化first数组
	for (int i = 1; i <= n; ++i) dis[i] = inf; dis[1] = 0;
	for (int i = 1; i <= n; ++i) first[i] = -1;	
	
	for (int i = 1; i <= m; ++i)
	{//first数组存储的就是 每个顶点i (u[i]) 的按输入顺序的最后u-v边
		scanf("%d %d %d", u+i, v+i, w+i);
		next[i] = first[u[i]];// 创建的时候先放next再放first 
		first[u[i]] = i;// 使用的时候先看first在用next循环 
	}
	
	//	Bellman-Ford的队列优化
	q = 1; book[q] = true; que.push(q);//入队 
	while (!que.empty())
	{
		p = que.front(); que.pop();
		
		int k = first[p];
		while (k != -1)
		{// 扫描当前结点p 所有的边
			if (dis[v[k]] > dis[u[k]] + w[k])
			{// 结点p 相连的所有结点 v[k]
				dis[v[k]] = dis[u[k]] + w[k];
				if (!book[v[k]])
				{// 结点Vk未出现在队列中入队
					q = v[k]; book[q] = true; que.push(q);
				}
			}
			k = next[k];
		}
		
		book[p]	 = false;//可能多次入队 
	}
	
	
	for (int i = 1; i <= n; ++i)
	{
		printf(i == 1 ? "%d" : " %d", dis[i]);
	}	
	return 0;
}

测试数据同3.1一样:
5 5
2 3 2
1 2 -3
1 5 5
4 5 2
3 4 3
测试结果:
0 -3 -1 2 4

Bellman-Ford算法还有SPFA算法优化,若比较感兴趣,可自行学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值