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