【学习笔记】基础算法——Floyd

Floyd 是什么?

Floyd 是一种图论算法,全源最短路,可以在 \(O(n^3)\) 的时间内求出所有 \(x\)\(y\) 的最短路。一般用于 \(n\) 比较小并且为稠密图的情况下。

Floyd 的求解

首先简单看一下 Floyd 的定义:\(f_{k,i,j}\) 表示当前只考虑前 \(k\) 个点存在的情况下,\(i\)\(j\) 的最短路距离。最终所有的都跑完以后,\(f_{n,i,j}\) 表示的就是 \(i\)\(j\) 的最短路,如果走不通则数值为 \(\infty\)

接下来就是具体的求解方案了。先分别枚举 \(k,i,j\)。然后我们要尝试转移 \(dp_{k,i,j}\),这就要利用到 \(dp_{k-1}\) 中存储的信息了。由于 \(dp_{k}\) 相比于 \(dp_{k-1}\) 只多出了一个元素,所以只需要考虑路径中出现 \(k\) 节点的情况,那么我们就让 \(k\) 节点为中转站,让 \(dp_{k,i,j} = dp_{k-1,i,k} + dp_{k-1,k,j}\) 即可。但还需要考虑继承的情况,对吧,所以改一改式子变为 \(dp_{k,i,j} = \min( dp_{k,i,j} , dp_{k-1,i,k} + dp_{k-1,k,j} )\) 即可。

但是考虑到 \(O(n^3)\) 的空间复杂度还是比较巨大的,通常无法接受,因此考虑压维,直接删去 \(k\) 这一维度。那么转移式就变成了 \(dp_{i,j} = \min( dp_{i,j} , dp_{i,k} + dp_{k,j} )\)。当然了,\(k\) 还是需要枚举的,只不过省下了一维空间而已。这样空间复杂度就变为 \(O(n^2)\) 了,一般都能够接受的。

顺带提一嘴,有的时候,比方说 \(n=2000\),可以说是跑不了 Floyd,对吧,但还是可以尝试一下。如果只需要考虑能不能到达而不需要记录最短路径,那么完全可以使用 bitset 进行压缩。这样时间复杂度就能有一个 \(\frac{1}{32}\) 左右的常数,足以冲过去。

最后粘一下 Floyd 的核心代码:

for(int k=1;k<=n;k++)for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]);

是不是特别 easy?

例题分析

灾后重建。

这题挺好玩的。

考虑先把所有边连上,先跑一通 Floyd。但是点最开始全部都是“未开启”状态。

然后每次给定一个时间后,由于时间绝对是单调不减的,所以可以用一个 \(now\) 变量记录,就想指针一样,扫一遍,往后继续推进时间。每次扫到一个“未开启”的点,但时间已经达到时,需要赶紧执行 \(O(n^2)\) 的类 Floyd 操作(其实上等同于固定了 \(k\),毕竟新增一个点嘛)。输出的时候怎么办?简单,首先判断一下 \(x\)\(y\) 两个点都是“已开启”状态,然后输出 \(f\) 数组中对应存储的答案即可。

说实话,这个思路并不算很难,但是需要深入理解 Floyd 的本质才行。

#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 205 ;
const LL Inf = 0x3f3f3f3f3f3f3f3f;
LL n,m,Q,a[N],dp[N][N],now;
void update(int k){
    for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)
        dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]);return;
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>a[i];
    memset(dp,0x3f,sizeof(dp));
    for(int i=1;i<=n;i++)dp[i][i]=0;
    for(int i=1;i<=m;i++){
        LL x,y,z;cin>>x>>y>>z;x++,y++;
        dp[x][y]=min(dp[x][y],z),dp[y][x]=min(dp[y][x],z);
    }
    cin>>Q;
    while(Q--){
        LL x,y,t;cin>>x>>y>>t;x++,y++;
        while(a[now+1]<=t&&now<n)update(++now);
        cout<<((dp[x][y]==Inf||a[x]>t||a[y]>t)?-1:dp[x][y])<<"\n";
    }
    return 0;
}

再整一道

来哈,咱就非得给你再整一道

注意到,如果这个点删掉无所谓,那当然是在删掉这个点后的路径长度,还和存在这个点的一样,而不是会变小。因此依次判断,记录上一个存活点 \(lst\),然后不断往后枚举,如果遇到一个必须存活的点就将其扔进 vector,并更新 \(lst\) 即可。

思路简单清晰,代码也很简洁。

#include<bits/stdc++.h>
#define LL long long
#define pb push_back
using namespace std;
const int N = 105 , M = 1e6+5;
LL n,dp[N][N],lst,m,p[M],sum;vector<int> ans;
int main(){
    cin>>n;
    memset(dp,0x3f,sizeof(dp));
    for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)
        {char c;cin>>c;if(c=='1')dp[i][j]=1;}
    for(int i=1;i<=n;i++)dp[i][i]=0;
    for(int k=1;k<=n;k++)for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]);
    cin>>m;for(int i=1;i<=m;i++)cin>>p[i];lst=p[1],sum=0;ans.pb(p[1]);
    for(int i=2;i<=m;i++){
        sum+=dp[p[i-1]][p[i]];
        if(dp[lst][p[i]]<sum)lst=p[i-1],ans.pb(lst),sum=dp[lst][p[i]];
    }
    ans.pb(p[m]);cout<<ans.size()<<"\n";for(int u:ans)cout<<u<<" ";cout<<"\n";
    return 0;
}

简单总结

Floyd 这个东西的用处其实上并不算很广,毕竟它的时间复杂度比较炸裂,有 \(O(n^3)\),如果 \(n\) 到了个几千上万,基本上就是走不通的了。做 Floyd 的题,千万不要指望它直接把“Floyd”几个大字写在脑门上,它需要你去深入理解 Floyd 的本质,拆分,思考,才能做出来。

posted @ 2025-08-09 09:53  嘎嘎喵  阅读(23)  评论(0)    收藏  举报