最短路径问题明白纸

written by: 东篱下の悠然
1. Floyd-Warshall(多源最短路径问题)

问题重述:加权有向图中允许所有节点作为中转,如何求出任意两点之间的最短路径?

算法基本思想:最开始只允许经过一号点作为中转,接下来只允许经过一号点和二号点进行中 转……允许经过所有点进行中转,求任意两点之间的最短路径。(动态规划思想的一种体现)

实现过程:将每个点作为当前两点之间的中转点,不断更新任意两点之间的最短路径。

#include <bits/stdc++.h>

int n, m, e[50][50];
int main(){
	
int inf = 9999999;//存储一个我们认为是正无穷的值,其实只要大于边数的平方(m * m)即可
	
//初始化地图 
	scanf("%d%d", &n, &m);//输入节点个数和路径条数 
	for(int i = 1; i <= n; i++)
	for(int j = 1; j <= m; j++){
		if(i == j) e[i][j] = 0;//规定节点自己到节点自己的权为0
		else e[i][j] = inf; 
	} 
	
//输入权值,就是已有的节点到节点的距离 
	int a, b, r;
	for(int i = 1; i <= m; i ++){
		scanf("%d%d%d", &a, &b, &r);
		e[a][b] = r;
	} 
	
//Floyd-Warshall核心部分,只有五行,真是伟大
	for(int i = 1; i <= n; i ++)//允许经过i点中转 
		for(int j = 1; j <= n; j++)
			for(int k = 1; k <= n; k++)
			
			if (e[j][k] > e[j][i] + e[i][k])
			e[j][k] = e[j][i] + e[i][k];//更新最短路径 
	
//输出最终更新结果
	for(int i = 1; i <= n; i ++){
		for(int j = 1; j <= n; j++){
			printf("%2d", e[i][j]);
		}
		printf("\n");
	} 
	return 0;		
}

输入节点个数m和路径条数n,接下来n行输入两个节点的编号和它们之间的权值,用空格隔开。最终会返回一个m行m列的表格,表示j号节点(表格的第j行)到k号节点(表格的第k列)交界处的值即为最短路径。
总结:这种方法实现起来非常容易,可以求任意两边的最短路径,时间复杂度为O(N3)(有点慢)。此算法可以求指定两点之间的最短路径或指定一个点到其他所有点的最短路径。它可以处理带有“负权边”(即路径上的值小于零)的图,但不可以处理带有“负权环”的图(可能没有最短路径)。

上面介绍的floyd-warshall算法中有一个明显的缺点就是时间复杂度太高。接下来介绍一种可以将时间复杂度降低一个维度的算法:

2. Dijkstra算法(单源最短路径问题)

问题重述:加权有向图中允许所有节点作为中转,如何求出指定一点到其他所有点的最短路径?

算法基本思想:从顶点a开始,首先找到离a最近(权值最小)的顶点,设为x,再从x出发找所有的出边边,设点x直接连接了y和z,再来研究x到y这条路是不是让a到y的路程变得更短。在这里设点a到其他顶点的距离全部存储在dis数组中,点a能直接到的点则设初始值为相应的权,若不能直接到的点设置为inf。如果a到x 加 x到y 小于 a直接到y,则更新dis[y]的值,这个过程的专业术语叫“松弛”。

实现过程:找到离源点最近的点,以该点进行扩展并不断松弛dis中的数值,最终dis中的数值就是源点到其他点的最短路径。

#include <bits/stdc++.h>
int n, m, e[50][50];
int main(){
	
	int inf = 9999999;//存储一个我们认为是正无穷的值
	
//初始化地图 
	scanf("%d%d", &n, &m);//输入节点个数和路径条数 
	for(int i = 1; i <= n; i++)
	for(int j = 1; j <= m; j++){
		if(i == j) e[i][j] = 0;//规定节点自己到节点自己的权为0
		else e[i][j] = inf; 
	} 
	
//输入权值,就是已有的节点到节点的距离 
	int a, b, r;
	for(int i = 1; i <= m; i ++){
		scanf("%d%d%d", &a, &b, &r);
		e[a][b] = r;
	} 
	
//初始化book数组,一号顶点到其余点的初始路程 
	int dis[50];
	for(int i = 1; i <= n; i++)
		dis[i] = e[1][i]; 
	
//book数组初始化
	int book[50];
	for(int i = 1; i <= n; i ++)
		book[i] = 0;
	book[1] = 1;
	
//Dijkstra核心代码
	for(int i = 1; i <= n; i ++){
		//找离一号最近的点 
		int u;
		int min = inf;
		for(int j = 1; j <= n; j++){
			if(book[j] == 0 && dis[j] < min){
			 min = dis[j];
			 u = j; 
			}
		}
		book[u] = 1;
		for(int k = 1; k <= n; k++){
			if(e[u][k] < inf){
				if(dis[k] > dis[u] + e[u][k])
					dis[k] = dis[u] + e[u][k];
			}
		}
	}
//输出最终更新结果
	for(int i = 1; i <= n; i ++)
		printf("%d ",dis[i]);
	return 0;		
}

Dijkstra算法时间复杂为O(N2) 并且可以用“堆”来优化使复杂度降到O(logN)
缺点:不能解决带有负权边的图:

3. Bellman-Ford算法:解决负权边

问题重述:解决单源最短路径有负权边的问题。

算法基本思想:因为最短路径上最多有n-1条边,所以在此算法中最多有n-1个阶段,对每一条边进行松弛操作。每进行一次松弛就会有一些顶点已经求得最短路,即这些顶点的dis初始的“估计值”变为最短的“确定值”,并在此后这些点的最短路保持不变,不再受后续的松弛操作影响。

#include <bits/stdc++.h>

int n, m, e[50][50];
int main(){
	
	int inf = 9999999;//存储一个我们认为是正无穷的值
	
//读入边 
	int u[10], v[10], w[10];//记录边的信息,例如从u[i]到v[i]这条边权值为w[i] 
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; i++)
		scanf("%d%d%d", &u[i], &v[i], &w[i]); 

//初始化dis数组,就是一号顶点到其余各个顶点的初始路程
	int dis[10];
	for(int i = 1; i <= n; i ++)
		dis[i] = inf;
	dis[1] = 0;
	
//核心语句
	for(int k = 1; k <= n - 1; k++)
		for(int i = 1; i <= m; i++){
			if(dis[v[i]] > dis[u[i]] + w[i])
			   dis[v[i]] = dis[u[i]] + w[i];
		} 
	
	for(int i = 1; i <= n; i++)
	printf("%d ",dis[i]);
	
	return 0;
}

此外,它还可以检测一个图是否含有负权回路,如果进行完n-1轮松弛后,仍然存在

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

的情况,仍然可以继续松弛,则存在负权回路。因为如果一个图没有负权回路,则最短路径所包含的边最多n-1条,即n-1轮松弛后路径不再变化。

//核心语句
	for(int k = 1; k <= n - 1; k++)
		for(int i = 1; i <= m; i++){
			if(dis[v[i]] > dis[u[i]] + w[i])
			   dis[v[i]] = dis[u[i]] + w[i];
		} 
//检测负权回路
	for(int i = 1; i <= m; i++){
		if(dis[v[i]] > dis[u[i]] + w[i])
			printf("含有负权回路");

总结:该算法的时间复杂度为* O(NM)*。因为上述代码只考虑最坏情况,最多会执行n-1轮松弛。实际上可以进行优化,可以添加一个check变量标记数组dis是否发生变化,若没有发生变化则可以跳出循环:

#include <bits/stdc++.h>

int n, m, e[50][50];
int main(){
	
	int inf = 9999999;//存储一个我们认为是正无穷的值
	
//读入边 
	int u[10], v[10], w[10];//记录边的信息,例如从u[i]到v[i]这条边权值为w[i] 
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; i++)
		scanf("%d%d%d", &u[i], &v[i], &w[i]); 

//初始化dis数组,就是一号顶点到其余各个顶点的初始路程
	int dis[10];
	for(int i = 1; i <= n; i ++)
		dis[i] = inf;
	dis[1] = 0;
	
//核心语句
	for(int k = 1; k <= n - 1; k++){
		int check = 0;
		
		for(int i = 1; i <= m; i++){
			if(dis[v[i]] > dis[u[i]] + w[i]){
				dis[v[i]] = dis[u[i]] + w[i];
				check = 1;
			}	   
		} 
		if(check == 0) break;
	}
//检测负权回路		
	for(int i = 1; i <= m; i++){
		if(dis[v[i]] > dis[u[i]] + w[i])
			printf("含有负权回路");
			
		else {
			for(int i = 1; i <= n; i++)
			printf("%d ",dis[i]);
		}
	} 
	return 0;
} 

Bellman-Ford优化算法每次仅对发生变化了的相邻的边进行松弛操作,但若要知道当前哪些点的最短路径发生了变化,可以用一个队列来维护这些点:

4. Bellman-Ford队列优化

算法基本思想:每次选取队首顶点u,对u的所有出边进行松弛操作,若源点到该顶点的松弛操作成功就将顶点加入队尾(会有重复顶点入队)。对该顶点所有出边松弛完毕后就将它出队,取下一个队首…直到队空为止。(与广度优先搜索bfs非常相似)

说到有重复顶点入队,需要用一个标记数组book去重:

#include <bits/stdc++.h>

int n, m, e[50][50];
int book[10];//全局变量默认填充0,表示节点都不在队列中 
int main(){
	
	int inf = 9999999;//存储一个我们认为是正无穷的值
	int dis[10] = {0};
	
//读入边 
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; i++)
		dis[i] = inf;
	dis[1] = 0;
	
	int first[6], next[8];//first = n + 1; next = m + 1
	for(int i = 1; i <= n; i++)
		first[i] = -1;
		
//读入每一条边
	int u[8], v[8], w[8];//要比m大1 
	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;
	} 
	 
//一号顶点入队
	int head = 1, tail = 1;
	int queue[100] = {0};
	queue[tail] = 1;
	tail ++;
	book[1] = 1;
	
	while(head < tail){//队不空 
	
		int k = first[queue[head]];//需要处理的队首 
		
		while(k != -1){//扫描当前顶点所有出边
		
			if(dis[v[k]] > dis[u[k]] + w[k]){//判断是否松弛成功
				dis[v[k]] = dis[u[k]] + w[k]; //更新顶点到v[k]的路程 
				
				if(book[v[k]] == 0){//不在队列中 
					queue[tail] = v[k];
					tail ++;
					book[v[k]] = 1; 
				} 
			}
			k = next[k];
		} 
		//出队
		book[queue[head]] = 0;
		head ++;
	}	

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

也可以不用book数组,只是每次更新后就要将用该顶点遍历队列找重复值,时间复杂度是O(N) ,使用book数组则复杂度为O(1)

♥如有谬误还请指正~~蟹蟹♥
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值