【总结】2020暑假集训--最短路

最短路

在带权图 G = ( V , E ) G = (V, E) G=(V,E) 中,每条边都有一个权值 w i w_i wi ,即边的长度。路径的长度为路径上所有边权之和。
最短路问题是指:求图中某点到另一点的最短路径的长度。 例如下图:
在这里插入图片描述
从点 1 1 1到点 4 4 4的最短路为 5 5 5,路径是 1 → 2 → 3 → 4 1\rightarrow 2\rightarrow 3\rightarrow 4 1234。 特殊地,对于无权图或者带权图每条边权值相同的图,最短路可以通过BFS得到。
但是对于一般的带权图,就不能通过BFS得到最短路了,因为会出现两条边加起来小于一 边的情况,比如上图中的 1 → 2 → 3 1\rightarrow 2\rightarrow 3 123长度小于 1 → 3 1\rightarrow 3 13,这样如果BFS先访问到3,把最短路记录为 1 → 3 1\rightarrow 3 13的长度5,后续也不再更改,那就不对了。
接下来我们\解决最短路问题的相关算法,通过接下来学到的算法,我们就可以解决带权图最短路问题了。

多源最短路–floyd

接下来介绍一种算法Floyd算法可以解决最短路问题,所谓多源则是它可以求出以每个点为起点到其它每个点的最短路。
不过其实有一种特殊情况是求不出最短路的,就是有负环的情况,此时可以不断在这个负环上转圈,所以负环上的点之间最短路为负无穷,这种情况下一般也只是判断出负环,不做其它处理,Floyd 算法无法判断这样的情况,所以就在没有负环的情况下使用,负环的判断在之后的算法中会给出方法。

Floyd算法是-种利用动态规划的思想、计算给定的带权图中任意两个顶点之间最短路径的算法。无权图可以直接把边权看
作1。
我们用 d p [ k ] [ i ] [ j dp[k][i][j dp[k][i][j]示 i i i j j j能经过1 ~ k的点的最短路。那么实际上 d p [ 0 ] [ i ] [ j ] dp[0][i][j] dp[0][i][j]就是原图,如果 i , j i,j i,j之间存在边,那么
i , j i,j i,j之间不经过任何点的最短路就是边长,则 i , j i, j i,j之间的最短路为无穷大。
那么对于 i , j i,j i,j之间经过1 ~ k的最短路 d p [ k ] [ i ] [ j ] dp[k][i][j] dp[k][i][j]可以通过经过1 ~ k - 1的最短路转移过来。
●如果不经过第k个点,那么就是 d p [ l k − 1 ] [ i ] [ j ] dp[lk - 1][i][j] dp[lk1][i][j]
●如果经过第k个点,那么就是 d p [ k − 1 ] [ i ] [ k ] + d p [ k − 1 ] [ k ] [ j ] dp[k - 1][i][k] + dp[k - 1][k][j] dp[k1][i][k]+dp[k1][k][j]
所以就有转移
d p [ k ] [ i ] [ j ] = m i n ( d p [ k − 1 ] [ i ] [ j ] , d p [ k − 1 ] [ i ] [ k ] + d p [ k − 1 ] [ k ] [ j ] ) \displaystyle dp[k][i][j] = min(dp[k - 1][i][j], dp[k - 1][i][k] + dp[k - 1][k][j]) dp[k][i][j]=min(dp[k1][i][j],dp[k1][i][k]+dp[k1][k][j])

我们仔细分析, d p [ k ] dp[k] dp[k] 只能由 d p [ k − 1 ] dp[k - 1] dp[k1]转移过来。并且 d p [ k − 1 ] [ i ] [ k ] = d p l [ k ] [ i ] [ k ] dp[k - 1][i][k] = dpl[k][i][k] dp[k1][i][k]=dpl[k][i][k],因为 i i i k k k的最短路中间肯定不会
经过 k k k。同理, d p [ k − 1 ] [ k ] [ j ] = d p [ k ] [ k ] [ i ] dp[k - 1][k][j] = dp[k][k][i] dp[k1][k][j]=dp[k][k][i]
那么转移实际上变成了
d p [ k ] [ i ] [ j ] = m i n ( d p [ k − 1 ] [ i ] [ j ] , d p [ k ] [ i ] [ k ] + d p [ k ] [ k ] [ j ] ) dp[k][i][j]=min(dp[k−1][i][j],dp[k][i][k]+dp[k][k][j]) dp[k][i][j]=min(dp[k1][i][j],dp[k][i][k]+dp[k][k][j])
这时候,我们尝试把 k k k这一维去掉,就用 d p [ i ] [ j ] dp[i][j] dp[i][j] 来表示 i , j i, j i,j之间的最短路,那么转移变成了
∀ 1 ≤ k ≤ n ∀1 \leq k \leq n 1kn d p [ i ] [ j ] = m i n ( d p [ i ] [ j ] , d p [ i ] [ k ] + d p [ k ] [ j ] ) dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]) dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j])

我们写出最终的 Floyd 的形式,这也是常用的写法,优化了一维的空间。并且写法更加简单。如果理解了动态规划的思想,你就一定明白了为什么枚举的中间点 k k k一定要写在最外面。没有理解这一点,很容易把 3 3 3个循环的顺序弄错了。

#include<cstdio>
#include<iostream>
#include<cmath>
#include<cstring>
using namespace std;
const int MAXN=505;
int floyd[MAXN][MAXN];
int s,t;
int main(){
	memset(floyd,0x3f,sizeof(floyd));
	int n,m;
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		floyd[i][i]=0;
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			scanf("%d",&floyd[i][j]);
		}
	}
	for(int k=1;k<=n;k++){
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				if(floyd[i][j]>floyd[i][k]+floyd[k][j]){
					floyd[i][j]=floyd[i][k]+floyd[k][j];
				}
			}
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			printf("%d ",floyd[i][j]); 
		}
		printf("\n");
	}
return 0;
}

这里在使用的时候因为邻接矩阵存的是距离,所以需要把不连通的部分赋值为一个很大的整数,避免其对答案产生影响,这样也可以根据这个无穷大判断两点间是否可达。

如果需要记录路径,可以记录一下每两个点之间最后是被哪个点更新的,然后一条路就可以被拆成两半不断递归找到路径了。

#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
const int MAXN=205;
int G[MAXN][MAXN];
int dis[MAXN]; 
bool vis[MAXN];
int pre[MAXN];
void print(int x){
	if(pre[x]==0){
		printf("%d",x);
		return;
	}
	print(pre[x]);
	printf(" %d",x);
}
int main(){
	int n,m,s,t;
	scanf("%d %d",&n,&m);
	int a,b;
	memset(G,0x3f,sizeof(G));
	memset(dis,0x3f,sizeof(dis));
	for(int i=1;i<=m;i++){
		scanf("%d %d",&a,&b);
		G[a][b]=1;
	}
	scanf("%d %d",&s,&t);
	dis[s]=0;
	for(int i=1;i<=n;i++){
		int minn=0x3f3f3f3f;
		int k=0;
		for(int j=1;j<=n;j++){
			if(vis[j]==0 && dis[j]<minn){
				minn=dis[j];
				k=j;
			}
		}
		if(k==0) continue;
		vis[k]=1;
		for(int j=1;j<=n;j++){
			if(dis[j]>dis[k]+G[k][j]){
				dis[j]=dis[k]+G[k][j];
				pre[j]=k;
			}
		}
	}
	if(s==t){
		printf("0\n%d",s);
		return 0;
	}
	if(dis[t]>m){
		printf("no solution");
		return 0;
	}
	printf("%d\n",dis[t]);
	print(t);
return 0;
}

单源最短路–dijkstra

算法概念

解决单源最短路径问题常用 Dijkstra 算法,用于计算一个顶点到其他所有顶点的最短路径。Dijkstra 算法的主要特点是以起点为中心,逐层向外扩展(这一点类似于 bfs,但是不同的是,bfs 每次扩展一个层,但是 Dijkstra 每次只会扩展一个点),每次都会取一个最近点继续扩展,直到取完所有点为止。

注意:Dijkstra 算法要求图中不能出现负权边。

算法流程

我们定义带权图 G G G 所有顶点的集合为 V V V,接着我们再定义已确定从源点出发的最短路径的顶点集合为 U U U,初始集合 U U U 为空,记从源点 s s s 出发到每个顶点 v v v 的距离为 d v d_v dv ,初始 d s = 0 d_s=0 ds=0接着执行以下操作:
1.从 V − U V-U VU中找出一个距离源点最近的顶点 v v v,将 v v v加入集合 U U U
2.并用 d v d_v dv 和顶点 v v v连出的边来更新和 v v v相邻的、不在集合 U U U中的顶点的 d d d,这一步称为松弛操作。
3.重复步骤 1 和 2,直到 V = U V=U V=U 或找不出一个从 s s s出发有路径到达的顶点,算法结束。
如果最后 V ≠ U V \neq U V=U,说明有顶点无法从源点到达;否则每个 d i d_i di ​ 表示从 s s s出发到顶点 i i i的最短距离。
Dijkstra 算法的时间复杂度为 O ( V 2 ) \mathcal {O}(V^2) O(V2),其中 V V V表示顶点的数量。

算法演示

在这里插入图片描述
初始每个顶点的 d d d设置为无穷大 inf ⁡ \inf inf,源点 M M M d M d_M dM设置为 0 0 0。当前 U = ∅ U=\emptyset U= V − U V-U VU d d d最小的顶点是 M M M。从顶点 M M M 出发,更新相邻点的 d d d
在这里插入图片描述
更新完毕,此时 U = { M } U=\{M\} U={M} V − U V-U VU d d d 最小的顶点是 W W W。从 W W W 出发,更新相邻点的 d d d
在这里插入图片描述
更新完毕,此时 U = { M , W } U=\{M,W\} U={M,W} V − U V-U VU d d d 最小的顶点是 E E E。从 E E E出发,更新相邻顶点的 d d d在这里插入图片描述
更新完毕,此时 U = { M , W , E } U=\{M, W, E\} U={M,W,E} V − U V-U VU d d d 最小的顶点是 X X X。从 X X X出发,更新相邻顶点的 d d d在这里插入图片描述
更新完毕,此时 U = { M , W , E , X } U=\{M,W,E,X\} U={M,W,E,X} V − U V-U VU d d d 最小的顶点是 D D D。从 D D D 出发,没有其他不在集合 U U U 中的顶点。在这里插入图片描述
此时 U = V U=V U=V,算法结束,单源最短路计算完毕。

代码实现

#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
const int MAXN=2505;
int G[MAXN][MAXN];
int dis[MAXN]; 
bool vis[MAXN];
int main(){
	int n,m,s,t;
	scanf("%d %d %d %d",&n,&m,&s,&t);
	int a,b,c;
	memset(G,0x3f,sizeof(G));
	memset(dis,0x3f,sizeof(dis));
	for(int i=1;i<=m;i++){
		scanf("%d %d %d",&a,&b,&c);
		G[a][b]=G[b][a]=c;
	}
	vis[s]=0;
	dis[s]=0;
	for(int i=1;i<=n;i++){
		int minn=0x3f3f3f3f;
		int k=0;
		for(int j=1;j<=n;j++){
			if(vis[j]==0 && dis[j]<minn){
				minn=dis[j];
				k=j;
			}
		}
		if(!k) continue;
		vis[k]=1;
		for(int j=1;j<=n;j++){
			if(dis[j]>dis[k]+G[k][j]){
				dis[j]=dis[k]+G[k][j];
			}
		}
	}
	printf("%d",dis[t]);
return 0;
}

时间复杂度 O ( V 2 ) \mathcal {O}(V^2) O(V2),考虑优化

优化

首先是松弛与 k k k点相邻的点操作:

for(int j=1;j<=n;j++){
	if(dis[j]>dis[k]+G[k][j]){
		dis[j]=dis[k]+G[k][j];
	}
}

很容易想到用邻接表进行优化,只枚举相邻的即可

还可以更快?

我们把眼光转向寻找距离原点最近的点的那一波操作,找最值,想到什么?没错,优先队列(堆)

最终优化:

#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
using namespace std;
const int MAXN = 2505;
bool vis[MAXN];
int n, m, dis[MAXN];
struct edge{  
	int v,w; 
	edge(){};
	edge(int V, int W) {
		v=V;
		w=W;
	}
};
struct node{
	int u,dis_;
	node(){}
	node(int U,int D) {
		u=U;
		dis_=D;
	}
	bool operator<(const node a)const{return dis_>a.dis_;}
};
priority_queue<node> q; 
vector<edge> cost[MAXN]; 
void add(int su,int sv,int scost) { 
	cost[su].push_back(edge(sv,scost));
	cost[sv].push_back(edge(su,scost));
}
void Dijkstra(int s){
	memset(dis,0x3f,sizeof(dis));
	dis[s]=0;
	q.push(node(s,0));
	while(!q.empty()) {
		int t=q.top().u; 
		q.pop();
		if(vis[t]){
			continue;
		}
		vis[t]=true;
		for(int i=0;i<cost[t].size();i++){
			int v=cost[t][i].v;
			if(dis[v]>dis[t]+cost[t][i].w){
				dis[v]=dis[t]+cost[t][i].w;
				q.push(node(v,dis[v]));
			}
		}
	}
}

int main() {
	int n,m;
	int s,t;
	scanf("%d %d %d %d", &n, &m,&s,&t);
	for(int i=1;i<=m;i++) {
		int t1,t2,t3;
		scanf ("%d %d %d",&t1,&t2,&t3);
		add(t1,t2,t3); 
	}
	Dijkstra(s); 
	printf("%d",dis[t]);
	return 0;
}

时间复杂度 O ( M l o g M ) , M \mathcal {O}(MlogM),M O(MlogM),M为边数

单源最短路–Bellman-Ford

曾今被我念成了 B e l l m a n − F l o y d Bellman-Floyd BellmanFloyd [手动狗头]

算法概念

Bellman-Ford算法:对每条边执行更新,迭代N-1次。
具体操作是对图进行最多n-1次松弛操作,每次操作对所有的边进行松弛,为什么是n-1次操作呢?这是因为我们输入的边不一定是按源点由近至远,万一是由远至近最坏情况就得n-1次,我们可以以一个单链 A → B → C → D A\rightarrow B\rightarrow C\rightarrow D ABCD来举例

初始化为全部距离原点为INF

在这里插入图片描述

假如现在输入边是 C → D , 3 C \rightarrow D,3 CD,3
在这里插入图片描述
然鹅没有任何一条边得到松弛

又输入 B → C , 2 B \rightarrow C,2 BC,2
在这里插入图片描述

然鹅依然没有任何一条边得到松弛

现在输入 A → B , 1 A \rightarrow B,1 AB,1
在这里插入图片描述
哎,好像可以了,只需要从A到B一路松弛下去,就OK了

在这里插入图片描述

好消息:Bellman-Ford可以应用于负权图

算法演示

正权图

初始化
在这里插入图片描述
D i s [ 1 ] = 0 Dis[1] = 0 Dis[1]=0
D i s [ 2 、 3 、 4 、 5 、 6 ] = ∞ Dis[2、3、4、5、6] = ∞ Dis[23456]=


对第1条边双向进行松弛之后

在这里插入图片描述
D i s [ 1 ] = 0 Dis[1] = 0 Dis[1]=0
D i s [ 2 ] = 1 Dis[2] = 1 Dis[2]=1
D i s [ 3 、 4 、 5 、 6 ] = ∞ Dis[3、4、5、6] = ∞ Dis[3456]=


对第2条边双向进行松弛之后

在这里插入图片描述

D i s [ 1 ] = 0 Dis[1] = 0 Dis[1]=0
D i s [ 2 ] = 1 Dis[2] = 1 Dis[2]=1
D i s [ 3 ] = 2 Dis[3] = 2 Dis[3]=2
D i s [ 4 、 5 、 6 ] = ∞ Dis[4、5、6] = ∞ Dis[456]=


对第3条边双向进行松弛之后
在这里插入图片描述
D i s [ 1 ] = 0 Dis[1] = 0 Dis[1]=0
D i s [ 2 ] = 1 Dis[2] = 1 Dis[2]=1
D i s [ 3 ] = 2 Dis[3] = 2 Dis[3]=2
D i s [ 4 、 5 、 6 ] = ∞ Dis[4、5、6] = ∞ Dis[456]=


对第4条边双向进行松弛之后

在这里插入图片描述
D i s [ 1 ] = 0 Dis[1] = 0 Dis[1]=0
D i s [ 2 ] = 1 Dis[2] = 1 Dis[2]=1
D i s [ 3 ] = 2 Dis[3] = 2 Dis[3]=2
D i s [ 4 ] = 5 Dis[4] = 5 Dis[4]=5
D i s [ 5 、 6 ] = ∞ Dis[5、6] = ∞ Dis[56]=


对第5条边双向进行松弛之后
在这里插入图片描述
D i s [ 1 ] = 0 Dis[1] = 0 Dis[1]=0
D i s [ 2 ] = 1 Dis[2] = 1 Dis[2]=1
D i s [ 3 ] = 2 Dis[3] = 2 Dis[3]=2
D i s [ 4 ] = 5 Dis[4] = 5 Dis[4]=5
D i s [ 5 ] = 10 Dis[5] = 10 Dis[5]=10
D i s [ 6 ] = ∞ Dis[6] = ∞ Dis[6]=


对第6条边双向进行松弛之后
在这里插入图片描述

D i s [ 1 ] = 0 Dis[1] = 0 Dis[1]=0
D i s [ 2 ] = 1 Dis[2] = 1 Dis[2]=1
D i s [ 3 ] = 2 Dis[3] = 2 Dis[3]=2
D i s [ 4 ] = 5 Dis[4] = 5 Dis[4]=5
D i s [ 5 ] = 8 Dis[5] = 8 Dis[5]=8
D i s [ 6 ] = ∞ Dis[6] = ∞ Dis[6]=


对第7条边双向进行松弛之后
在这里插入图片描述

D i s [ 1 ] = 0 Dis[1] = 0 Dis[1]=0
D i s [ 2 ] = 1 Dis[2] = 1 Dis[2]=1
D i s [ 3 ] = 2 Dis[3] = 2 Dis[3]=2
D i s [ 4 ] = 5 Dis[4] = 5 Dis[4]=5
D i s [ 5 ] = 8 Dis[5] = 8 Dis[5]=8
D i s [ 6 ] = 11 Dis[6] = 11 Dis[6]=11


对第8条边双向进行松弛之后
在这里插入图片描述
D i s [ 1 ] = 0 Dis[1] = 0 Dis[1]=0
D i s [ 2 ] = 1 Dis[2] = 1 Dis[2]=1
D i s [ 3 ] = 2 Dis[3] = 2 Dis[3]=2
D i s [ 4 ] = 5 Dis[4] = 5 Dis[4]=5
D i s [ 5 ] = 8 Dis[5] = 8 Dis[5]=8
D i s [ 6 ] = 11 Dis[6] = 11 Dis[6]=11

到此,这个图的单源最短路已全部求出

现在让我们看看负权图吧:

负权图

在这里插入图片描述
D i s [ 1 ] = 0 Dis[1] = 0 Dis[1]=0
D i s [ 2 、 3 、 4 、 5 、 6 ] = ∞ Dis[2、3、4、5、6] = ∞ Dis[23456]=

对第1条边双向进行松弛之后


在这里插入图片描述
D i s [ 1 ] = 0 Dis[1] = 0 Dis[1]=0
D i s [ 2 ] = 1 Dis[2] = 1 Dis[2]=1
D i s [ 3 、 4 、 5 、 6 ] = ∞ Dis[3、4、5、6] = ∞ Dis[3456]=


对第2条边双向进行松弛之后
在这里插入图片描述
D i s [ 1 ] = 0 Dis[1] = 0 Dis[1]=0
D i s [ 2 ] = 1 Dis[2] = 1 Dis[2]=1
D i s [ 3 ] = 2 Dis[3] = 2 Dis[3]=2
D i s [ 4 、 5 、 6 ] = ∞ Dis[4、5、6] = ∞ Dis[456]=


对第3条边双向进行松弛之后
在这里插入图片描述
D i s [ 1 ] = 0 Dis[1] = 0 Dis[1]=0
D i s [ 2 ] = − 5 Dis[2] = -5 Dis[2]=5
D i s [ 3 ] = − 2 Dis[3] = -2 Dis[3]=2
D i s [ 4 、 5 、 6 ] = ∞ Dis[4、5、6] = ∞ Dis[456]=

唉?dis[2]貌似有什么不对,-5怎么走出来的呀?不对,-5好像还真的走出来, 1 → 2 → 3 → 2 1\rightarrow2\rightarrow3\rightarrow2 1232就行了,发现了什么, 2 → 3 2\rightarrow3 23是一个“负环”,其实,在无向图中,凡是有负权边就相当于有负环了,因为可以在这条边上左右横跳,就可以让路程无限短。怎么办呢?那就不管它了呗,先换成有向图走完再说


对第3条边进行松弛之后
在这里插入图片描述

D i s [ 1 ] = 0 Dis[1] = 0 Dis[1]=0
D i s [ 2 ] = − 1 Dis[2] = -1 Dis[2]=1
D i s [ 3 ] = 2 Dis[3] = 2 Dis[3]=2
D i s [ 4 、 5 、 6 ] = ∞ Dis[4、5、6] = ∞ Dis[456]=


第4条边进行松弛之后
在这里插入图片描述

D i s [ 1 ] = 0 Dis[1] = 0 Dis[1]=0
D i s [ 2 ] = − 1 Dis[2] = -1 Dis[2]=1
D i s [ 3 ] = 2 Dis[3] = 2 Dis[3]=2
D i s [ 4 ] = 3 Dis[4] = 3 Dis[4]=3
D i s [ 5 、 6 ] = ∞ Dis[5、6] = ∞ Dis[56]=


对第5条边进行松弛之后
在这里插入图片描述

D i s [ 1 ] = 0 Dis[1] = 0 Dis[1]=0
D i s [ 2 ] = − 1 Dis[2] = -1 Dis[2]=1
D i s [ 3 ] = 2 Dis[3] = 2 Dis[3]=2
D i s [ 4 ] = 3 Dis[4] = 3 Dis[4]=3
D i s [ 5 ] = 8 Dis[5] = 8 Dis[5]=8
D i s [ 6 ] = ∞ Dis[6] = ∞ Dis[6]=


对第6条边双向进行松弛之后
在这里插入图片描述

D i s [ 1 ] = 0 Dis[1] = 0 Dis[1]=0
D i s [ 2 ] = − 1 Dis[2] = -1 Dis[2]=1
D i s [ 3 ] = 2 Dis[3] = 2 Dis[3]=2
D i s [ 4 ] = 3 Dis[4] = 3 Dis[4]=3
D i s [ 5 ] = 8 Dis[5] = 8 Dis[5]=8
D i s [ 6 ] = ∞ Dis[6] = ∞ Dis[6]=


对第7条边双向进行松弛之后
在这里插入图片描述
D i s [ 1 ] = 0 Dis[1] = 0 Dis[1]=0
D i s [ 2 ] = − 1 Dis[2] = -1 Dis[2]=1
D i s [ 3 ] = 2 Dis[3] = 2 Dis[3]=2
D i s [ 4 ] = 3 Dis[4] = 3 Dis[4]=3
D i s [ 5 ] = 8 Dis[5] = 8 Dis[5]=8
D i s [ 6 ] = 10 Dis[6] = 10 Dis[6]=10


对第8条边双向进行松弛之后在这里插入图片描述
D i s [ 1 ] = 0 Dis[1] = 0 Dis[1]=0
D i s [ 2 ] = − 1 Dis[2] = -1 Dis[2]=1
D i s [ 3 ] = 2 Dis[3] = 2 Dis[3]=2
D i s [ 4 ] = 3 Dis[4] = 3 Dis[4]=3
D i s [ 5 ] = 8 Dis[5] = 8 Dis[5]=8
D i s [ 6 ] = 10 Dis[6] = 10 Dis[6]=10

OK,求完了,等等,刚才不是还留下了一个负环的问题吗?

我们想想负环是不是可以无限松弛,那么我们就可以在算法跑完后再跑一遍,看看有没有可以继续松弛的,如果有,那么这个图一定有负环

代码

附赠路径输出

#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
const int MAXN=505;
struct edge{
	int u,v;
	int val;
}G[MAXN];
int n,m;
int dis[MAXN];
int pre[MAXN];
int w[MAXN];
int s,t;
void print(int x){
	if(pre[x]==0){
		printf("%d",x);
		return;
	}
	print(pre[x]);
	printf(" %d",x);
}
bool BellmanFord(){
	dis[s]=0;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			if(dis[G[j].v]>dis[G[j].u]+G[j].val){
				dis[G[j].v]=dis[G[j].u]+G[j].val;
				pre[G[j].v]=G[j].u;
			}
			else if(dis[G[j].v]==dis[G[j].u]+G[j].val){
				dis[G[j].v]=dis[G[j].u]+G[j].val;
				pre[G[j].v]=min(G[j].u,pre[G[j].v]);
			}
		}
	}
	bool flag=true;
	for(int i=1;i<=m;i++){//判负环
		if(dis[G[i].v]>dis[G[i].u]+G[i].val){
			return 0;
		}
	}
	return flag;
}
int main(){
	memset(dis,0x3f,sizeof(dis));
	scanf("%d %d",&n,&m);
	for(int i=1;i<=m;i++){
		scanf("%d %d %d",&G[i].u,&G[i].v,&G[i].val);
		pre[G[i].v]=G[i].u;
	}
	scanf("%d %d",&s,&t);
	if(!BellmanFord()){
		printf("No Solution");
	}
	else{
		printf("%d\n",dis[t]);
		print(t);
	}
return 0;
}

单源最短路–SPFA

它死了
NOI2018 Day 1,T1 出题人卡了 SPFA 并在讲课时说其死了。

算法概念

在Bellmanford算法中,有许多松弛是无效的。这给了我们很大的改进的空间。SPFA算法正是对Bellmanford算法的改进。它是由西南交通大学段丁凡1994提出的。它采用了队列和松弛技术。先将源点加入队列。然后从队列中取出一个点(此时该点为源点),对该点的邻接点进行松弛,如果该邻接点松弛成功且不在队列中,则把该点加入队列。如此循环往复,直到队列为空,则求出了最短路径。
判断有无负环:如果某个点进入队列的次数超过N次则存在负环 ( 存在负环则无最短路径,如果有负环则会无限松弛,而一个带n个点的图至多松弛n-1次)

算法流程

在 SPFA 算法中,使用 d i d_i di表示从源点到顶点 i i i的最短路,额外用一个队列来保存即将进行拓展的顶点列表,并用 i n _ q u e u e i in\_queue_i in_queuei 来标识顶点 i i i是不是在队列中。

  1. 初始队列中仅包含源点,且源点 s s s d s = 0 d_s=0 ds=0
  2. 取出队列头顶点 u u u,扫描从顶点 u u u出发的每条边,设每条边的另一端为 v v v,边 < u , v > <u,v> <u,v> 权值为 w w w,若 d u + w < d v d_u+w<d_v du+w<dv,则
    • d v d_v dv修改为 d u + w d_u+w du+w
    • v v v不在队列中,则将 v v v入队

重复步骤 2 2 2直到队列为空

最终 d d d 数组就是从源点出发到每个顶点的最短路距离。如果一个顶点从没有入队,则说明没有从源点到该顶点的路径。

如果进队次数超过 n n n次,那么说明图中存在负环。

算法思想

在一定程度上,也可以认为 SPFA 是由 BFS 的思想转化而来。从不含边权或者说边权为 1 1 1个单位长度的图上的 BFS,推广到带权图上,就得到了 SPFA。只是 BFS 能保证第一次访问就一定是最短路,而 SPFA 在每次更新了最短路以后又重新入队从而去更新后续结点的最短路。比如下图, 2 2 2搜到 3 3 3的时候会再次更新 3 3 3最短路。
在这里插入图片描述

代码:

#include<cstdio>
#include<iostream>
#include<queue>
#include<vector>
#include<cstring>
using namespace std;
const int MAXN=1000005;
struct node{
	int v;
	int w;
	node(){}
	node(int V,int W){
		v=V,w=W;
	}
};
int n,m,s,t;
vector<node> G[MAXN];
queue<int> que;
bool vis[MAXN];
int dis[MAXN];
void add(int u,int v,int w){
	G[u].push_back(node(v,w));
}
void spfa(){
	dis[s]=0;
	vis[s]=1;
	que.push(s);
	while(!que.empty()){
		int u=que.front();
		que.pop();
		vis[u]=0;
		for(int i=0;i<G[u].size();i++){
			int v=G[u][i].v,w=G[u][i].w;
			if(dis[v]>dis[u]+w){
				dis[v]=dis[u]+w;
				if(!vis[v]){
					que.push(v);
					vis[v]=1;
				}
			}
		}
	}
}
int main(){
	scanf("%d %d",&n,&m);
	int a,b,c;
	s=1,t=n;
	for(int i=1;i<=m;i++){
		scanf("%d %d %d",&a,&b,&c);
		add(a,b,c);
		add(b,a,c);
	}
	for(int i=1;i<=n;i++){
		dis[i]=0x3f3f3f3f;
	}
	spfa();
	printf("%d",dis[t]);
return 0;
}
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值