AIGC标识 关于floyd

废话

相信很多人会用 dijkstra 和 spfa ,但是 floyd 在稠密图的复杂度可能更低,某些题目甚至要在 floyd 的基础上在混入些神秘小操作,理解不深刻直接就g了。所以这篇文章 由deepseek老师 讲解 floyd。

后继

666 写这篇文章两天后的 CSP-S T2 真考了 floyd,成功的写对了循环顺序

注意到某选手(该选手的洛谷账号)和某选手(该选手的洛谷账号)在中国计算机学会于2026年举办的非专业级别软件能力认证提高级第二题里非常熟练地使用了妇孺皆知的N^3求最短路算法,不过值得注意的是,该选手的所写的floyd将中转点的枚举放到了循环最内侧,并且只跑了一遍。然后呢?

\[100pts \rightarrow 28+???pts \]

\[\text{1=} \rightarrow \text{???} \]

该选手衷心祝愿大家不再重蹈覆辙。

此致
警示后入

上课时学长讲"易错点",大家昏昏欲睡。为了醒瞌睡,学长使出看家本领,讲了一个故事。

一个在 CSP 的考生,T2 写 Floyd 以获得分数。CCF 的评测机有一个不成文的评分标准,即 for ijk 写法必须用循环跑上三遍。运行的时间是按次数计算的,这位考生计上心头,循环时少跑两遍。果然,代码常数大大减小,运行效率便大大提高。旁边的考生向他请教技巧。他毫不避讳,说:"你看,跑了三遍的 Floyd 和跑了一遍的有什么区别吗?少跑两次嘛。"旁边的考生诺诺,却与他渐渐疏远了。

CCF 主席偶尔抽查一下 Floyd 的正确性。一次抽查中,主席用特殊构造的数据测出 Floyd 正确性不够并责问我们这位考生时,他振振有词:"跑一遍和跑三遍不是一样保证了代码正确性吗?"主席只是淡淡地说:"你是一个不诚实的人,请你离开。"

为了成绩,他又参加另一场比赛。这位主考官打量了他半天,才说:"你就是那位只跑一遍 Floyd 的考生吧。对不起,我们不通过你的审核!"第二场、第三场……他屡屡碰壁。不仅如此,他的教练不久也要求他退竞,原因是他的"名声"对其他同学的成绩产生了不良影响。他就读的学校也专门找他谈话,希望他能转到其他学校去,因为他影响了学校的生源……万般无奈,他只好收拾行李搬到了另一所学校,一切重新开始。他痛心疾首地告诫准备参加 CSP 的考生:"在 CSP 写 Floyd,一定要写三遍呀!"

"这就是易错点!"学长厉声棒喝。我们不禁悚然动容,睡意全无。

Floyd 到底在干什么

一句话

Floyd 是动态规划,不是“最短路算法”里那种贪心/松弛的感觉。它的状态定义是:

f[k][i][j] = 只允许用编号 (1..k) 的节点作为中转点,从 (i) 到 (j) 的最短距离。

注意“中转点”三个字:起点 (i) 和终点 (j) 本身不受限制,受限制的是路径中间经过的点。

最后答案是 f[n][i][j]:允许所有点做中转。


从 DP 推导出三重循环

状态

f[k][i][j]:从 (i) 到 (j),路径中间只经过 (1..k) 这些点的最短距离。

转移

考虑第 (k) 个点用不用作中转:

  • 不用 (k) 中转:f[k][i][j] = f[k-1][i][j]
  • 用 (k) 中转:f[k][i][j] = f[k-1][i][k] + f[k-1][k][j]

取 min:

\[f[k][i][j] = \min\big(f[k-1][i][j],\ f[k-1][i][k] + f[k-1][k][j]\big) \]

滚动数组优化

发现 f[k] 只依赖 f[k-1],可以压掉第一维:

\[f[i][j] = \min(f[i][j],\ f[i][k] + f[k][j]) \]

这就是模板:

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

初始化

  • f[i][i] = 0
  • f[u][v] = w(有边)
  • 其他 f[i][j] = INF

注意 f 初始就是邻接矩阵,不是一堆 INF


为什么 k 必须最外层?

这是初学者最容易背错的地方。核心原因:滚动数组的正确性依赖于“第 k 层转移时,f[i][k] 和 f[k][j] 还没被第 k 层更新过”。

如果 k 放最内层:

for (int i = 1; i <= n; i++)
    for (int j = 1; j <= n; j++)
        for (int k = 1; k <= n; k++)   // ← 错!
            f[i][j] = min(f[i][j], f[i][k] + f[k][j]);

那么当你在算 f[i][j] 用某个 (k) 时,f[i][k] 可能已经被同一次外层循环里的另一个中转点修改过了,相当于你把“经过多个点”的信息混进了“只经过当前 k”的转移里,DP 的层次就乱了,可能算错(有些图还能对,但理论上不保证)。

记住一句话:k 是“允许使用的中转点集合”的维度,它必须是最外层的“阶段”。 i、j 是状态的两个下标,放在内层随意。


Floyd vs Dijkstra vs SPFA

Floyd Dijkstra SPFA
求什么 任意两点最短路 单源最短路 单源最短路
边权 可负(不能有负环) 非负 可负(不能有负环)
复杂度 (O(N^3)) (O(M\log N)) 平均快,最坏 (O(NM))
适合 (N) 小、多次询问任意两点、需要动态加点 单源、非负 单源、有负边

Floyd 有两个别的算法替代不了的地方:

  1. 一次性求全源最短路:(Q) 次任意两点询问,如果 (Q) 很大,Floyd 一次 (O(N^3)) 就全答了,比跑 (Q) 次 Dijkstra 划算。
  2. 动态加点(本题):图不是一开始就完整,节点按时间“解锁”,Floyd 的 DP 结构天然支持——每解锁一个点,就用它做一次 (O(N^2)) 的松弛,总 (O(N^3))。Dijkstra/SPFA 做不到这么优雅。

神秘题目

戳我

回到本题:动态 Floyd

本题的图是“随时间逐渐解锁”的。Floyd 的 DP 语义刚好对上了:

加入时间 \(\le t\) 的村庄 = 允许它们作为中转点。

所以维护一个指针 now,当 rec[now] <= t 时:

// 用 now 作为新的中转点,做一次 Floyd 的"第 now 层转移"
for (int i = 1; i <= n; i++)
    for (int j = 1; j <= n; j++)
        f[i][j] = min(f[i][j], f[i][now] + f[now][j]);

这就是只做了 Floyd 的一层。因为 rec 非降、询问 t 非降,now 单调递增,每个点恰好做一次,总 (O(N^3))。

关键点:f 从初始化起就是邻接矩阵,此后只被 Floyd 更新,不被覆盖。这样 f[i][now] 在 now 加入时,已经是“从 (i) 到 now 只经过已解锁点”的最短路——正是 Floyd 转移需要的东西。

对了还有代码

Code

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

namespace IO{
    struct INID{INID(){ios::sync_with_stdio(false),cin.tie(nullptr);}};
    static INID INI_D;
    inline int gc(){return cin.rdbuf()->sbumpc();}
    inline void pc(char c){cout.rdbuf()->sputc(c);}
    template<typename T>void rd(T &x){
        x=0;int f=1,c=gc();
        while('0'>c||c>'9'){if(c=='-')f*=-1;c=gc();}
        while('0'<=c&&c<='9'){x=x*10+c-'0';c=gc();}
        x*=f;
    }template<typename T>void wt(T x){
        if(x<0)pc('-');
        static int d,tp,stk[40];tp=0;
        do{d=x%10;stk[++tp]=(d<0?-d:d)+'0';x/=10;}while(x);
        while(tp)pc(stk[tp--]);
    }
}using namespace IO;

/*-tpdef------------------------------*/

typedef long long ll;

/*-const------------------------------*/

constexpr int Q=5e4+5;
constexpr int N=2e2+5;
constexpr int M=N*N;

/*-def--------------------------------*/

int n,m,q;
int rec[N];
int f[N][N];
int g[N][N];

/*-ds---------------------------------*/

/*-alg--------------------------------*/

inline void global(){
    // freopen(".in","r",stdin);
}

namespace tqy{
    inline void init(){
        memset(f,0x3f,sizeof f);
        memset(g,0x3f,sizeof g);
        rd(n),rd(m);
        for(int i=1;i<=n;++i){
            rd(rec[i]);
            f[i][i]=0;
        }
        for(int i=1;i<=m;++i){
            int u,v,w;
            rd(u),rd(v),rd(w);//attention!! input = 0index
            ++u,++v;
            f[u][v]=w;
            f[v][u]=w;
        }
    }
    inline void solve(){
        int now=1;
        rd(q);
        for(int i=1;i<=q;++i){
            int u,v,t;
            rd(u),rd(v),rd(t);
            ++u,++v;
            while(now<=n&&rec[now]<=t){
                for(int j=1;j<=n;++j){
                    for(int k=1;k<=n;++k){
                        if(f[j][now]!=0x3f3f3f3f&&f[now][k]!=0x3f3f3f3f)f[j][k]=min(f[j][k],f[j][now]+f[now][k]);
                    }
                }
                ++now;
            }
            if(rec[u]<=t&&rec[v]<=t&&f[u][v]!=0x3f3f3f3f){
                wt(f[u][v]),pc('\n');
            }else{
                pc('-'),
                pc('1'),
                pc('\n');
            }
        }
    }
}

signed main(){
    global();
    int T=1;
    while(T--)tqy::init(),tqy::solve();
    return 0;
}
/*
*/


总结记忆

  • Floyd = DP:f[k][i][j] 允许前 k 个点做中转。
  • k 在最外层是因为它是对应 DP 的阶段维度。
  • 初始化 f 就是邻接矩阵。
  • 它的不可替代场景:全源最短路 + 动态加点(本题)。

理解到这个层次,本题为什么“每解锁一个点,就用它做一次 (O(N^2)) 的三重循环(其中一层固定为 now)”就非常自然了——那正是 Floyd 的一层 DP 转移。

posted @ 2026-10-29 17:14  tqytql  阅读(9)  评论(4)    收藏  举报