Floyd(多源最短路)证明 + 输出路径

108 篇文章 0 订阅
17 篇文章 0 订阅

因为Dijkstra算法求的是到起点的最短路,但是但是咱们有时候需要求任意两个点之间的最短路,这时候写n遍Dijkstra就会特别麻烦,咱们有位名叫Floyd的大佬就用动态规划的思想创造了Floyd算法,下面看一下这个算法具体的代码

void floyd(){
    for (int k = 1; k <= n;k++)
        for (int i = 1; i <= n;i++)
            for (int j = 1; j <= n;j++)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

那么这么简单的代码是怎么推过来的呢

假设d[k][i][j]表示从i点只经过1-k这些中间点到j点的最短距离,那么
d[k][i][j] = min(d[k-1][i][j],d[k-1][i][k] + d[k-1][k][j])
那么第一维也可以去掉,至于为什么会去掉呢,现在就我和我同学的讨论来简述一下吧,大家都知道,dp优化掉一维
的做法是屡见不鲜的,从01背包到最长不下降子序列,仿佛哪个dp都能优化掉一维一样,我一开始看y总说把数组优
化掉一维的时候是很懵逼的,真的不明白为啥这个状态没更新删了去就没事,现在随着做题量上去点了也有点领悟了
这个就类似于滚动数组,你用完这个状态后可能就没用了,就比如说这个Floyd算法的d[k][i][j]吧,咱们都是从
k-1给转移过来的,那么k-2呢,是不是就没用了,以后也不会用了,因为咱们最后要的是d[n][i][j],所以说就可以
把k-2的状态给用到k的状态上来,就类似于废物利用吧,然后优化一维的前提是这个状态是k-1的,那么咱们把前面
的一维去掉之后这个递推式子就变成了d[i][j] = min(d[i][j],d[i][k] + d[k][j]),再空间上是减小了不少的空
间复杂度的,但是这个式子为什么会正确呢,我想到了一种情况就是前面的一维不是k-1的情况,就是假如现在的k是
3,i是1,j是3,现在的d[1][3]已经从d[k-1][1][3]更新成为了d[k][1][3]了,但是又是为什么都这样了算法得出
的结果还是正确的呢,不妨想想,d[k][i][j] < d[k-1][i][j],所以说用前者来更新不就更好了吗,因为
d[k][i][j]表示的是从i号点到j号点中间只经过1~k号点的最短距离,d[k-1][i][j]表示的是从i号点到j号点中间只
经过1~k-1号点的最短距离中间的这些点是不包括起点和终点的,那么根据这个状态的表述,d[k][i][k]和
d[k-1][k][j]是可以接起来的,而且是比原来的更好的,这里可以看作是小贪一波哈哈哈,就转移过去了,而且后面
的d[i][k]和d[k][j]一定是动态更新的,假设i到j的最短路径上的点的序号的最大值是x,当k=x遍历完后,这个
d[i][j]就更新完了,这就是我的理解,可能有些不对的地方,还请大家批评指正
最终的结果就是d[i][j] = min(d[i][j],d[i][k] + d[k][j])

应用

这个floyd算法不仅可以算多源最短路,而且他还能判断负环,就是先把每个dis[i][i]设置成0,如果有负环的话,某个点的最后的值就会小于0,可以看一下这个题

Wormholes

在探索他的许多农场时,农民约翰发现了一些惊人的虫洞。虫洞是非常奇特的,因为它是一个单向路径,送你到它的目的地的时间,是在你进入虫洞之前!FJ 的每个农场都包括N (1 ≤ N ≤ 500) 字段,方便编号为 1.。N, M (1 ≤ M ≤ 2500) 路径,和 W (1 ≤ W ≤ 200) 虫洞。

由于FJ是一个狂热的时间旅行的球迷,他想做以下:从某个领域开始,通过一些路径和虫洞旅行,并在他最初离开前一段时间回到起跑场。也许他能:)见到自己。

为了帮助 FJ 了解这是否可能,他将向您提供其农场F(1 ≤ F ≤ 5)的完整地图。没有路径将需要超过 10,000 秒的行程,没有虫洞可以使 FJ 回到时间超过 10,000 秒。

输入

行 1: 单个整数, F.F农场描述如下。
每个农场的1号线:三个空间分离整数:N、M和W
线2。每个农场的M+1:分别描述三个空间分离数字(S、E、T):S 和E之间的双向路径,需要T秒才能穿越。 两个字段可能通过多个路径连接。
行M+2.。每个农场的M+W+1:分别描述三个空间分离数字(S、 E、 T):从S到E的单向路径,也将旅行者向后移动T秒。

输出

行1.。F:对于每个农场,如果 FJ 能够实现他的目标,则输出"是",否则输出"否"(不包括报价)。

示例输入

2
3 3 1
1 2 2
1 3 4
2 3 1
3 1 3
3 2 1
1 2 3
2 3 4
3 1 8

样本输出

NO
YES

提示

对于农场 1,FJ 无法及时返回。
对于农场 2,FJ 可以在 1->2->3->1 周期前返回时间,在他离开前 1 秒返回起点位置。他可以从周期的任何地方开始完成这个任务。

这个题的意思就是说存在虫洞可以穿越到以前,问有没有可能过了一段时间以后再回到现在这个时间之前的时间点,那么咱们可以把过了一段时间这个时间看成正权边,把虫洞看成负权边,然后找有没有负权回路就行了,我自己用的是spfa算法判断的负环,现在先提供一种用Floyd算法判断负环的代码

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;

const int INF = 0x3f3f3f3f;
const int MAXV = 1000;
int n, m, w;
int dis[MAXV][MAXV];
int flag;

bool Floyd() {//核心代码
	for (int k = 1; k <= n; k++) {
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= n; j++)
				if (dis[i][k] + dis[k][j] < dis[i][j]) {
					dis[i][j] = dis[i][k] + dis[k][j];
				}
			if (dis[i][i] < 0) {
				return true;
			}
		}
	}
	return false;
}
int main() {
	int f;
	scanf("%d", &f);
	while (f--) {
		scanf("%d%d%d", &n, &m, &w);
		memset(dis,0x3f,sizeof dis);
		for(int i = 1; i <= n; i ++ )
			dis[i][i] = 0;
		int u, v, val;
//		for (int i = 1; i <= m; i++) {//正权边
//			scanf("%d%d%d", &u, &v, &val);
//			if(dis[u][v] > val) dis[u][v] = dis[v][u] = val;
//		}
		for(int i=1; i<=m; i++) { //输入正权边
			scanf("%d%d%d",&u,&v,&val);
			if(dis[u][v]>val) {
				dis[u][v]=val;
				dis[v][u]=val;
			}
		}
		for (int i = 1; i <= w; i++) {//负权边
			scanf("%d%d%d", &u, &v, &val);
			dis[u][v] = -val;
		}
		if (Floyd()) puts("YES");
		else puts("NO");
	}
	return 0;
}

下面看一下纸我写的spfa算法吧

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int N = 510,M = 2510*2,W = 210;
int w[N][N]; 
bool st[N];
int cnt[N],dis[N];
int n,m,f;
queue<int> q;
bool spfa() {
	memset(dis, 0x3f, sizeof dis);
	while(!q.empty()) q.pop();
	for (int i = 1; i <= n; i++) {
		q.push(i);
		st[i] = true;
	}
	while (q.size()) {
		int t = q.front();
		q.pop();
		st[t] = false;
		for(int i=1;i<=n;i++){
			if(w[t][i] != 0x3f3f3f3f){
				if(dis[i] > dis[t] + w[t][i]){
					dis[i] = dis[t] + w[t][i];
					cnt[i] = cnt[t] + 1;
					if(cnt[i] >= n) return true;
					if(!st[i]){
						st[i] = true;
						q.push(i);
					}
				}
			}
		}
	}
	return false;
}
int main() {
	int T;
	scanf("%d",&T);
	while(T--) {
		scanf("%d%d%d",&n,&m,&f);
		memset(cnt,0,sizeof cnt);
		memset(w,0x3f,sizeof w);
		for(int i=1; i<=m; i++) {
			int a,b,c;
			scanf("%d%d%d",&a,&b,&c);
			if(w[a][b] > c){
				w[a][b] = w[b][a] = c;
			}
		}
		for(int i=1; i<=f; i++) {
			int a,b,c;
			scanf("%d%d%d",&a,&b,&c);
			c = -c;
			if(w[a][b] > c) w[a][b] = c;
		}
		if(spfa()) puts("YES");
		else puts("NO");
	}
	return 0;
}

Floyd输出路径

核心代码
for(int k=1;k<=n;k++)
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			if(dis[i][j] > dis[i][k] + dis[k][j]){
				dis[i][j] = dis[i][k] + dis[k][j];
				path[i][j] = k;
			}

这个k相当于一个中间点,就是 i 到 j 的最短路,那该怎么输出最短路呢

初始化pre[i][j]= j;
int u = st;//st是起点,end是终点
while(u != pre[u][end]){
cout << u << endl;
u = pre[u][end];
}
cout << u << endl;

这个就是不断的更新起点,然后有很多个中间点,一个一个的找,最后就找到终点了,为什么最后找到的是终点呢,是因为一开始咱们初始化了呀

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宇智波一打七~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值