← 目录 第 42 / 60 节 全部 60 节

42 · 最短路:Dijkstra 与 Floyd

把 BFS 的队列换成按 dist 排序的小根堆:Dijkstra 靠“每次取 dist 最小的点”做到 O((n + m) log n),Floyd 用三重循环一次算出所有点对。重点是懒删除、INF 的选法,以及 k 循环为什么必须最外层。
提高-预计 75 分钟DijkstraFloyd堆优化
本节出现的新函数 / 新语法(先看懂再用)

第 26 节已经会用 BFS 求最短路了,但它有一条隐形前提:每条边的代价都相等。有了这条前提,“先到”才等于“最近”。题目一旦给出边权 3、7、1 这样的数字,这个前提就碎了 —— 走两步的路径可能比走一步的代价还小。

于是要换判据:不比“第几层”,比“当前累计代价谁最小”。把 BFS 的队列换成按 dist 排序的容器,就得到 Dijkstra;把“一次算一个源点”改成“一次算所有点对”,就得到 Floyd。本节就这两个。

Dijkstra 的全部正确性压在一句话上:每次取出当前 dist 最小的点,它的 dist 已经不会再变小了。边权非负是这句话成立的唯一理由。
算法解决什么复杂度对边权的要求什么时候用
BFS(第 26 节)单源O(n + m)全部相等(通常为 1)无权图、最少步数
Dijkstra(堆优化)单源O((n + m) log n)非负绝大多数单源题,m 到 10⁶
Floyd全源(所有点对)O(n³)允许负边,不能有负环n ≤ 300~500,或要任意两点距离
Bellman-Ford / SPFA单源 + 判负环O(nm)允许负边有负权边时(本节只提一句)

先记三个数字,它决定了你该选谁。① n = 10⁵、m = 2×10⁵ 时,一次堆优化 Dijkstra 约 (n + m)·log m ≈ 3×10⁵ × 18 ≈ 5×10⁶ 次操作,稳过。② n = 500 时 Floyd 是 1.25×10⁸ 次内层操作,约 0.3 秒,能过。③ n = 1000 时 Floyd 是 10⁹,必超时 —— 而同样规模下跑 1000 次 Dijkstra 是 10⁹ 级,也不行。所以“n 小用 Floyd”里的“小”是 500,不是 5000。

本节的代码里只多了一个东西:一个能按“距离最小”自动排序的队列。它由 priority_queue 加 pair 加 greater 拼成,三样都要看准。

新函数拆解 · priority_queue 存 pair(堆里放 (距离, 点号))
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> pq; pq.push({d, u});
组成含义 / 格式要求
pair<int,int>把“距离”和“点号”打成一个元素。Dijkstra 的堆里一个元素就是一条“到 u 的距离是 d”的记录,不是点本身 —— 同一个 u 可以有多条记录。
第一个参数(元素类型)必须是 pair,因为堆里要同时带走距离和点号。只放点号的话,取出来还得回表查 dist,而 priority_queue 不支持按 dist 排序。
第二个参数 vector<pair<int,int>>底层容器。想换比较器就必须把它写出来 —— 这是 priority_queue 模板最反直觉的地方:三个参数得写全。
第三个参数 greater<...>比较器。greater 让“更小的”排在堆顶,于是 top() 是距离最小的记录,这正是“小根堆”。
pq.push({d, u})花括号直接构造 pair,等价于 push(make_pair(d, u))。
pq.top()取堆顶,返回 const 引用,不能直接改。要拆开用结构化绑定:auto [d, u] = pq.top();(C++17)。
pq.pop()删掉堆顶,返回 void。所以 top 和 pop 必须成对写,顺序不能反。
不能遍历、不能删任意元素只有 top / push / pop。Dijkstra 靠“重复入堆 + 取出时判旧值”绕开“修改已入堆元素”这个做不到的操作。
最小可运行示例
示例
// 小根堆:(距离, 点号),距离小的先出
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> pq;

pq.push({5, 3});                     // 到 3 号点距离 5
pq.push({2, 7});                     // 到 7 号点距离 2
pq.push({2, 4});                     // 距离同为 2,点号小的在前

while (!pq.empty()) {
    auto [d, u] = pq.top();          // C++17 结构化绑定,拆成两个变量
    pq.pop();
    cout << u << "(" << d << ") ";   // 输出 4(2) 7(2) 3(5)
}
注意

元素类型和比较器必须匹配。距离用 long long 时整个写法都要改成 pair<long long,int> 配 greater<pair<long long,int>>,漏改一个就是满屏模板报错。

同一个点会在堆里出现多次(每次 dist 被改小就 push 一份)。这不是 bug,是“懒删除”的代价,取出时用 if (d != dist[u]) continue; 过滤即可。

新函数拆解 · greater 对 pair 的比较(字典序,first 优先)
greater<pair<A,B>> // 等价于 a > b,按字典序:先比 first,first 相等再比 second
组成含义 / 格式要求
字典序比较pair 的 > 是先比 first,first 相等才比 second。greater 把它倒过来用,于是“更小的”排在堆顶。
所以 first 必须是距离堆顶 = 距离最小的记录。写成 {u, d} 就变成“点号最小的先出堆”,距离完全没参与比较,“取最小即确定”的依据当场消失。
first 相同时比 second距离相同的两条记录按点号升序出堆。这只影响同距离点之间的先后,不影响答案。
为什么不能写 greater<int>元素是 pair,比较器必须能比较 pair。greater<int> 和元素类型对不上,报错会指到模板深处。
等价写法一:负数priority_queue<pair<int,int>> pq; 配 push({-d, u}),用负号把大根堆当小根堆用。少写两个模板参数,但每个 d 前都要加负号,漏一次就是灾难。
等价写法二:自定义结构体元素是自己写的 struct 时没有现成的 greater,要重载 operator< 或写函数对象(第 28 节),方向与 sort 的 cmp 相反。
最小可运行示例
示例
pair<int,int> a = {3, 9}, b = {3, 2}, c = {4, 1};
cout << (a > b) << " " << (a > c) << "\n";   // 1 0:first 相等时看 second;3 < 4 所以 a < c

// 所以小根堆里 {3,2} 先于 {3,9},两者都先于 {4,1}
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> pq;
pq.push({9, 3});        // ✗ 把点号写在了 first:堆按 9 排序,距离 3 根本没参与
pq.push({3, 9});        // ✓ first = 距离 3,second = 点号 9
// 错写法里 {9,3} 和 {3,9} 比的是两个 first(9 与 3),也就是两个"点号"
注意

greater 在 <functional> 里,bits/stdc++.h 已包含。

记住一句话:堆里第一个字段永远是排序依据。Dijkstra 里它必须是距离。

例题 · 【模板】单源最短路径(标准版)洛谷 P4779普及/提高-去洛谷做这题 ↗

给定一个 n 个点、m 条有向边的带非负权图,请计算从 s 出发到每个点的最短距离。数据保证从 s 出发能到达任意点。

输入格式:第一行三个正整数 n、m、s;第二行起 m 行,每行三个非负整数 u、v、w,表示从 u 到 v 有一条权值为 w 的有向边。

输出格式:一行 n 个空格分隔的非负整数,第 i 个数表示 s 到 i 的距离。

数据范围:1 ≤ n ≤ 10⁵,1 ≤ m ≤ 2×10⁵,s = 1,1 ≤ u, v ≤ n,0 ≤ w ≤ 10⁹,0 ≤ Σw ≤ 10⁹。

输入: 4 6 1 1 2 2 2 3 2 2 4 1 1 3 5 3 4 3 1 4 4 输出: 0 2 4 3
例题怎么想:BFS 换成堆,只改一处

① dist 的定义和 BFS 一样。dist[v] 表示“当前已知的 s 到 v 最短距离”。dist[s] = 0,其余设成一个极大的数 INF。注意是极大,不是 0 —— 全 0 等于宣布“所有点都已经到达且距离为 0”,后面一条边都松弛不动。

② 重复做一件事:取出未确定的点里 dist 最小的那个 u,用它松弛所有出边。“松弛”就是三角不等式:if (dist[u] + w < dist[v]) dist[v] = dist[u] + w;。BFS 里这一步是 dist[v] = dist[u] + 1,因为不用比也知道更优。

③ 为什么取最小就能“确定”。假设弹出的 u 的 dist 还不是真正的最短距离 d(u)。在 s→u 的真实最短路上,从 s 开始找第一个还没被确定的点 y。y 之前的点都已确定并松弛过,所以 dist[y] = d(y)。因为边权非负,最短路越往后越长,所以 d(y) ≤ d(u);又因为 u 是当前未确定点里 dist 最小的,所以 dist[u] ≤ dist[y]。串起来:d(y) ≤ d(u) < dist[u] ≤ dist[y] = d(y),矛盾。∎ “边权非负”只用在这一步,这是全节最值得记住的一句话。

④ 用堆代替线性扫描。朴素写法每次在 n 个点里找最小是 O(n²);把 (dist, u) 丢进小根堆,top() 就是最小,代价 O(log n)。

⑤ 懒删除。priority_queue 不能改已入堆的值,也不能删任意元素,所以 dist 变小时就再 push 一份新的,旧记录留在堆里。取出时判断 if (d != dist[u]) continue; —— 对不上说明这条是过期记录,扔掉。

⑥ 复杂度。每条边最多触发一次 push,所以最多 push m + 1 次;堆大小 ≤ m + 1;每次 push/pop 是 O(log m)。总复杂度 O((n + m) log m),写成 O((n + m) log n) 也一样。

骨架:六步
// 骨架:初始化 → 源点入堆 → 弹堆顶 → 懒删除 → 松弛 → 新值入堆
vector<long long> dist(n + 1, INF);
dist[s] = 0;
pq.push({0, s});
while (!pq.empty()) {
    auto [d, u] = pq.top(); pq.pop();
    if (d != dist[u]) continue;                 // 过期记录,扔掉
    for (auto &e : g[u]) {                      // 只扫 u 的出边,每条边一生只被扫一次
        int v = e.first, w = e.second;
        if (dist[u] + w < dist[v]) {            // 三角不等式
            dist[v] = dist[u] + w;
            pq.push({dist[v], v});              // 新值入堆,旧值不管
        }
    }
}

把骨架补全就得到下面的程序。两个细节值得先看:邻接表存的是 pair<int,int>(终点 + 边权),所以不用 struct Edge;距离用 long long,虽然本题 Σw ≤ 10⁹ 用 int 也够,但换一道题(边权 × 点数)就会爆,统一 long long 省心。

完整代码(C++17)
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m, s;
    cin >> n >> m >> s;

    // 邻接表:g[u] 里每一项是 (终点, 边权)
    vector<vector<pair<int,int>>> g(n + 1);
    for (int i = 0; i < m; ++i) {
        int u, v, w;
        cin >> u >> v >> w;
        g[u].push_back({v, w});        // 有向边,只加一次
    }

    const ll INF = (ll)4e18;           // 4e18 + 4e18 仍在 long long 内
    vector<ll> dist(n + 1, INF);
    dist[s] = 0;                       // 源点到自己是 0

    // 小根堆:first 是距离,堆顶永远是"当前距离最小的记录"
    priority_queue<pair<ll,int>, vector<pair<ll,int>>, greater<pair<ll,int>>> pq;
    pq.push({0, s});

    while (!pq.empty()) {
        auto [d, u] = pq.top();        // C++17:把 pair 拆成 d 和 u
        pq.pop();
        if (d != dist[u]) continue;    // 懒删除:这是 u 的旧记录

        for (auto &e : g[u]) {         // 引用遍历,不复制 pair
            int v = e.first, w = e.second;
            if (dist[u] + w < dist[v]) {          // 绕 u 更快
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});            // 旧记录留在堆里,靠上面那行过滤
            }
        }
    }

    for (int i = 1; i <= n; ++i)
        cout << dist[i] << (i == n ? '\n' : ' ');
    return 0;
}
逐行拆解
代码片段含义与理由
vector<vector<pair<int,int>>> g(n + 1);邻接表,每项是 (终点, 边权)。这里用 pair 而不是 struct Edge,因为只有两个字段、没有别的语义。构造函数给了 n + 1,下标 1..n 直接用。
const ll INF = (ll)4e18;INF 要满足两件事:比任何可能的最短路都大,且 INF + w 不溢出。4e18 加 1e9 远小于 long long 上限 9.22×10¹⁸,所以 dist[u] + w 不会变成负数。
vector<ll> dist(n + 1, INF);全部先当不可达。这一行的关键在于不是 0:初值给 0,所有点都“已到达且距离 0”,任何 dist[u] + w < dist[v] 都不成立,程序输出一行 0。
dist[s] = 0;源点到自己是 0。别漏,漏了源点也是 INF,堆里第一条记录就是 (INF, s),后面全乱。
priority_queue<pair<ll,int>, vector<pair<ll,int>>, greater<pair<ll,int>>> pq;三段拼出来的小根堆。first 是距离,所以 top() 给的是“当前最小距离的记录”。把 greater 换成默认的 less,就变成每次取最大,答案全错。
pq.push({0, s});只把源点入堆,不是把所有点都塞进去。堆里只该有“距离被更新过”的记录。
auto [d, u] = pq.top(); pq.pop();先取出再删除。结构化绑定等价于 ll d = pq.top().first; int u = pq.top().second;,但只调一次 top()。
if (d != dist[u]) continue;懒删除。u 的距离可能被改小过好几次,堆里躺着好几份 (d, u),只有 d == dist[u] 的那份是最新的。删掉这行答案仍正确,但过期记录也会去扫一遍出边,度数大的点被改多次时可能 TLE。
for (auto &e : g[u])引用遍历,不复制 pair。e.first 是终点 v,e.second 是边权 w。写成 auto e 也能跑,只是多一次拷贝。
if (dist[u] + w < dist[v])松弛的三角不等式。dist[u] 此刻已是最优值,所以只需检查“绕过 u 这一条边”是否有改善 —— 这正是“每个点只处理一次”的依据。
pq.push({dist[v], v});新距离入堆。注意 push 的是 dist[v] 而不是 d + w(两者相等,但前者可读性更好,也不怕以后改写法)。
(i == n ? '\n' : ' ')n 个数一行,行末只换行。多一个空格在某些严格比对的题上会 WA。
Floyd:三重循环,一次算出所有点对

Dijkstra 解决“一个源点到所有点”。如果要所有点对的距离,跑 n 次 Dijkstra 是 O(n(m + n) log n),n = 500 时约 8.5×10⁸,太慢;而 Floyd 是 O(n³) = 1.25×10⁸,反而更快。稠密图上,Floyd 常常是更优的选择。

思路是动态规划。定义 fk(i, j) = 只允许把编号 1..k 的点当作中转点时,i 到 j 的最短距离。从 k−1 推到 k 只有两种选择:不用 k,还是 fk−1(i, j);用 k,就是 fk−1(i, k) + fk−1(k, j)。于是

fk(i, j) = min( fk−1(i, j), fk−1(i, k) + fk−1(k, j) )。

k 这一维可以滚掉:算 fk 时 fk−1(i, k) 与 fk−1(k, j) 的值和 fk 的那两个值相同(最短路经过 k 本身不会变得更短,除非有负环),所以直接就地覆盖成二维数组即可。这就是为什么 k 循环必须写最外层 —— 它是 DP 的阶段维。

Floyd 骨架
#include <bits/stdc++.h>
using namespace std;

const int N = 305;
const long long INF = (long long)4e18;
long long f[N][N];

int main() {
    int n, m;                      // n ≤ 300
    cin >> n >> m;
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j)
            f[i][j] = (i == j ? 0 : INF);          // ★ 自己到自己是 0
    for (int i = 0; i < m; ++i) {
        int u, v, w; cin >> u >> v >> w;
        f[u][v] = min(f[u][v], (long long)w);      // 重边取最小
    }
    for (int k = 1; k <= n; ++k)                   // ★ 阶段:必须最外层
        for (int i = 1; i <= n; ++i) {
            if (f[i][k] == INF) continue;          // 剪枝:i 到不了 k,这一层白算
            for (int j = 1; j <= n; ++j)
                if (f[i][k] + f[k][j] < f[i][j])
                    f[i][j] = f[i][k] + f[k][j];
        }
    // 到这里 f[i][j] 就是 i 到 j 的最短路
}

k 放里层会错,给一个具体反例。4 个点、3 条边 1→3、3→4、4→2,权都是 1,正确答案 d(1, 2) = 3。若写成 for i for j for k:算 (i = 1, j = 2) 时需要用 f[1][4],而 f[1][4](要等 j = 4 才算)此刻还是 INF,于是 f[1][2] 永远停在 INF。这类错误的表现是“一部分点对正确、一部分是 INF 或偏大”,极难靠小样例发现。

Floyd 还白送三件东西:①允许负权边(只要没有负环);②判负环 —— 跑完看 f[i][i] < 0,说明有负环(正常应该等于 0);③传递闭包 —— 把 min 换成 or、+ 换成 and,得到的就是“i 能否走到 j”,三重循环一个字不用改。

负权边为什么不能 Dijkstra

给一个三点反例,能一次看清问题:边 1→2 权 2,1→3 权 5,3→2 权 −4。真实答案是 d(2) = 1(绕 3 号点走)。Dijkstra 的执行顺序是:dist = [0, 2, 5],先弹出 2 号点(dist = 2 最小),把它当最终值;再弹出 3 号点,松弛 3→2 得到 1 —— 但 2 号点已经处理完了,答案停在 2。这一段描述的是带“弹出即确定”标记的经典实现(弹出后 vis[u] = true 并不再更新 u),本节代码没有这个标记,结论见第三段。

坏在哪一步?回到前面的证明:那句 d(y) ≤ d(u) 用的是“最短路越往后越长”,而这条性质只在边权非负时成立。有负边时,最短路可能越走越短,“先取出的最小点”就不再是确定的。

还要说清楚一个细节,很多资料含糊过去:本节这种“懒删除、不加 vis”的写法,在负权图上最终仍会给出正确答案,因为每次距离变小都会重新入堆,等价于堆版的 Bellman-Ford —— 但复杂度从 O((n + m) log n) 退化成 O(nm) 量级,SPFA 的卡法能把它卡死。而只要你为了提速加上“弹出即确定”(if (vis[u]) continue; vis[u] = true;),负权图上就是彻底的 WA。

所以结论不变,只是理由更精确:Dijkstra 的复杂度保证依赖非负权,正确性在带“确定”标记的实现里也依赖它。有负权边时用 Bellman-Ford / SPFA,并且用它顺手判负环。

最后一个容易忽视的推论:无向图里只要出现一条负权边,就一定有负环(这条边来回走一趟,总权变成负数,可以无限刷小),此时最短路根本不存在。所以看到“无向图 + 负边”要么题目保证没有,要么答案是 −∞,不要指望任何最短路算法给出有限值。

本节的 8 个坑,每个都有具体症状

dist 用默认值 0 初始化(全局数组、或 vector<ll> dist(n + 1)):所有点都“已到达且距离 0”,一条边也松弛不动,输出一整行 0 → WA。这是本节第一大坑。

INF 取得太小(如 1e9):真实最短路超过 1e9 时会被当成“不可达”,输出 1000000000 → WA。INF 取 INT_MAX:INF + w 溢出成负数,负数被当成更短的路 → WA。

删掉 if (d != dist[u]) continue;:答案仍对,但过期记录也会去扫一遍所有出边,高度数点被反复更新时工作量成倍上涨 → TLE。

pair 写成 {u, d}(点号在前):堆顶变成“点号最小的记录”,距离没参与比较,复杂度退化到 SPFA 级别,构造数据 → TLE;若你还加了“弹出即确定”的 vis,则直接 WA。

忘记写 greater(用默认大根堆):每次取当前最大距离的点,和“取最小即确定”完全相反。小数据可能因为反复入堆而碰巧收敛,一旦加 vis 就 WA —— 这种“样例能过”的隐患最危险。

Floyd 的 k 循环写进 i、j 里层:部分点对停在 INF 或偏大 → WA,而且小样例常常是过的。

Floyd 忘记 f[i][i] = 0:经过自己的转移全部失效(INF + x 还是 INF),答案偏大 → WA;反过来,用 0 初始化整个矩阵会得到“任意两点距离都是 0” → WA。

用 int 存 dist:n = 10³、w = 10⁴ 时单程才 10⁷,看着够;但 n、w 各放大十倍就是 10⁹ 出头,再加一条边就溢出成负数 → WA。统一 long long,代价只是一点内存。

自己动手复现

关掉页面,从空文件把 P4779 敲一遍并提交。

自测数据就用样例:4 6 1 加 6 条边,答案应为 0 2 4 3。手推一遍 Dijkstra 的出堆顺序(1 → 2 → 4 → 3),再和程序打印的对照。

改一改①:把 dist 的初值从 INF 改成 0,重跑样例。输出会变成一行 0 ——亲眼确认“默认 0 等于宣布所有点都可达”。

改一改②:删掉 if (d != dist[u]) continue;,在 while 里加一个计数器统计出堆次数。同一组数据对比加不加这一行时堆里到底弹了多少条过期记录。

改一改③:把 greater<...> 整段删掉(变成默认大根堆),重跑样例。答案可能仍然正确 —— 想清楚为什么(提示:每次改进都会重新入堆),以及为什么一旦加上“弹出即确定”的 vis 就会错。

改一改④:把 Floyd 代码里的 k 循环挪到 i、j 里面,用 4 个点、3 条边 1 3 / 3 4 / 4 2(权都是 1)测 d(1, 2)。正确答案 3,错写法给 INF。

数学补给 · 无穷大怎么选、复杂度怎么估、k 为什么必须在外层

先解决最容易出错的小事:INF 要同时满足三条。① 比任何可能的最短路都大;② INF + w 不溢出;③ INF + w 仍要远大于任何真实路径,这样“不可达”的点不会因为加了一次边权就变成“可达”。

若最短路长度的上界是 L = (n − 1)·wmax,取 INF > L 就够了。P4779 里 Σw ≤ 10⁹,所以 L ≤ 10⁹,INF = 10⁹ + 1 都行;但习惯上写 4e18。

选法数值两倍是否溢出适用
0x3f3f3f3f(int)10611095672122219134 < 2³¹−1,安全int 版最短路,最常用
INT_MAX(int)2147483647溢出成负数,危险不要用
4e18(long long)4×10¹⁸8×10¹⁸ < 9.22×10¹⁸,安全long long 版
1e18(long long)10¹⁸2×10¹⁸,安全也常用,但要确认 L < 10¹⁸

再看复杂度怎么估,这是选算法的唯一依据。记 (n + m) log m 里的 log 按 18 算(m = 2×10⁵):

nm一次 Dijkstran 次 Dijkstra(全源)Floyd n³
50010⁵1.7×10⁶8.5×10⁸1.25×10⁸
10³10⁴1.4×10⁵1.4×10⁸10⁹
10⁵2×10⁵5.4×10⁶5.4×10¹¹10¹⁵

表里最值得盯的是第一行:n = 500 的稠密图上,Floyd 比“跑 500 次 Dijkstra”快 7 倍。原因很朴素:Floyd 的内层只有一次加法、一次比较、一次赋值,而 Dijkstra 每次出堆都要做 log 级别的比较,常数大得多。所以“Dijkstra 一定比 Floyd 快”是错的 —— 看 n、m 的比例。

最后解释 k 循环的位置。Floyd 是一个 DP:阶段 k 表示“允许用编号 ≤ k 的点当中转”。转移只从 k−1 层来,所以必须按层推进、层层算完。k 放里层,等于在用“还没算好的中间结果”做转移,某些点对就会停留在旧值上。

换个角度也说得通:最短路满足三角不等式 d(i, j) ≤ d(i, k) + d(k, j)。松弛就是把违反它的估计值改小。Floyd 的第 k 轮做完时,所有“只允许 1..k 当中间点”的三角不等式都成立;k 从 1 跑到 n,所有可能的中转点都被检查过一遍,留下的就是最短路。

顺带两条能直接用的结论:① 跑完看 f[i][i] < 0 就是在判负环;② 把 (min, +) 换成 (or, and) 就得到传递闭包 —— 同一个三重循环骨架,两种语义。这也是“矩阵乘法式”算法的开端:Floyd 本质上是 (min, +) 半环上的矩阵乘方。

举一反三 · 想清楚这些再往下走
  1. 如果图里有 0 权边,Dijkstra 还对吗?回到证明里看“d(y) ≤ d(u)”这一步 —— 它能扛住 0 权边,为什么扛不住负权边?只差一个等号还是不等号?
  2. 要求“从 s 到每个点的最短路径条数”,该在 Dijkstra 的哪一步累加?为什么必须在“松弛成功”和“距离相等”两种情况分开处理,且相等时不能重新入堆?
  3. 如果每条边的权只有 1 和 2 两种,能不能不用堆,把它做成 O(n + m)?提示:把每条权为 2 的边中间插一个虚拟点,拆成两条权为 1 的边(拆点),再用 BFS。
  4. Dijkstra 里什么时候可以提前退出?如果只关心“s 到 t 的距离”,弹出 t 的那一刻能不能停?换成“只关心 s 到某 k 个点的距离”呢?
  5. 把 Floyd 的 (min, +) 换成 (max, min) 会得到什么?(提示:最大瓶颈路,答案是“路径上最小边权的最大值”。)为什么同一套循环换成 (max, +) 求最长路就不行了?
  6. 如果边权是浮点数,Dijkstra 还成立吗?哪些地方要改(d != dist[u] 这一句首当其冲)?
  7. 有 10⁵ 个点和 10⁵ 条边,但所有边的权都是非负整数且总和不超过 10⁶,能不能做得比 Dijkstra 更快?提示:桶排序式的 Dijkstra(Dial 算法),第 28 节的 0-1 BFS 是它在 W = 1 时的特例。
自测:能全部回答再进下一节
挑战题 · 邮递员送信
洛谷 P1629普及/提高-去洛谷做这题 ↗

邮局在 1 号点,要送 n − 1 件东西到 2..n 号点。城市里所有道路都是单行道,共 m 条。邮递员每次只能带一件东西,送完必须返回邮局。

求送完这 n − 1 件东西并最终回到邮局所需的最少总时间。

输入格式:第一行两个整数 n、m;接下来 m 行,每行三个整数 u、v、w,表示从 u 到 v 有一条耗时 w 的单行道。

输出格式:一行一个整数,表示最少总时间。

数据范围:1 ≤ n ≤ 10³,1 ≤ m ≤ 10⁵,1 ≤ w ≤ 10⁴,保证任意两点互相可达。

引导性提示(先只看这里)

每次出门的路线是 1 → i,回程是 i → 1,n − 1 次出差互不影响。所以总时间就是 Σ(d(1, i) + d(i, 1)),不要想成“一条大回路”。

d(1, i) 跑一次 Dijkstra 就全有了。d(i, 1) 要对 n − 1 个 i 各跑一次吗?想想上一节的反向建图。

把所有边方向掉个头,在反图上从 1 跑一次 Dijkstra —— 反图上 1 到 i 的最短路,就是原图上 i 到 1 的最短路。

注意答案的最大量级:单程最坏约 10³ × 10⁴ = 10⁷,累加 10³ 次就是 10¹⁰,必须 long long。

想看思路了?点开完整思路

拆开算。送第 i 件东西的过程是“1 → i,再 i → 1”,与其他件无关,所以总时间 = Σi=2..n ( d(1, i) + d(i, 1) )。两次 Dijkstra 就能得到全部 2(n − 1) 个值。

第一遍:正图。以 1 为源点跑 Dijkstra,得到 distA[i] = d(1, i)。

第二遍:反图。建一张把所有边反向的图,同样以 1 为源点跑 Dijkstra。反图上一条 1 → x1 → … → i 的路径,在原图上正好是 i → … → x1 → 1 的路径,长度相同。所以 distB[i] = d(i, 1)。

为什么不能只跑一遍。正图给出的是“从 1 出发到各点”,而“从各点回到 1”在有向图里是完全不同的问题(单行道,去得回不得)。对每个点各跑一次是 O(n m log n) = 10³ × 10⁵ 量级,会超时;反图把它压回一次。

复杂度。两遍堆优化 Dijkstra,O((n + m) log n),n = 10³、m = 10⁵,约 2×10⁵ × 17 ≈ 3.4×10⁶ 次操作。

为什么不用 Floyd。n = 10³ 时 10⁹ 次内层操作,超时;而且我们只需要 1 到各点、各点到 1 这两批值,Floyd 算的 10⁶ 个点对里绝大多数用不上。

还是不会?点开题解代码与拆解
参考代码
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
const int N = 1005;
const ll INF = (ll)4e18;

int n, m;
vector<vector<pair<int,int>>> g, rg;   // g: 正图, rg: 反图
ll distA[N], distB[N];

// 从 s 出发的单源最短路,结果写进 d[1..n]
void dijkstra(vector<vector<pair<int,int>>> &gr, int s, ll *d) {
    for (int i = 1; i <= n; ++i) d[i] = INF;   // 全部先当不可达
    d[s] = 0;
    priority_queue<pair<ll,int>, vector<pair<ll,int>>, greater<pair<ll,int>>> pq;
    pq.push({0, s});
    while (!pq.empty()) {
        auto [du, u] = pq.top();
        pq.pop();
        if (du != d[u]) continue;              // 懒删除:过期记录
        for (auto &e : gr[u]) {
            int v = e.first, w = e.second;
            if (du + w < d[v]) {
                d[v] = du + w;
                pq.push({d[v], v});
            }
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;
    g.resize(n + 1);
    rg.resize(n + 1);
    for (int i = 0; i < m; ++i) {
        int u, v, w;
        cin >> u >> v >> w;
        g[u].push_back({v, w});        // 正图:u → v
        rg[v].push_back({u, w});       // 反图:v → u,方向必须掉头
    }

    dijkstra(g, 1, distA);             // d(1, i)
    dijkstra(rg, 1, distB);            // 反图上从 1 出发 = 原图上 i 到 1

    ll ans = 0;
    for (int i = 2; i <= n; ++i)       // 1 号点是邮局自己,不算
        ans += distA[i] + distB[i];
    cout << ans << "\n";
    return 0;
}
关键片段拆解
片段作用
rg[v].push_back({u, w});全题的转折点。原边 u → v 在反图里存成 v → u,权值不变。这样“反图上 1 能到 i”就等价于“原图上 i 能到 1”,一次 Dijkstra 顶掉 n − 1 次。
void dijkstra(vector<vector<pair<int,int>>> &gr, int s, ll *d)同一个函数跑两张图:传引用避免复制整张图,数组用指针传入 ll *d,传 distA 这种全局数组时会自动退化成指针。
ans += distA[i] + distB[i];第 i 件东西的总耗时是“去 + 回”。i 从 2 开始,因为 1 号点是邮局,没有东西要送给自己。
ll ans = 0;最大量级:单程最坏 (n − 1) × w = 999 × 10⁴ ≈ 10⁷,累加 999 次约 10¹⁰ —— 超过 int 上限 2.1×10⁹,必须 long long。这是本题最容易漏的一处。
for (int i = 1; i <= n; ++i) d[i] = INF;每次调用先重置。这里没写 0 而是 INF:写 0 就等于宣布“所有点距离 0”,一条边都松弛不动。
if (du != d[u]) continue;懒删除。u 的距离每被改进一次就会 push 一份记录,堆里可能躺着好几份,只有 du == d[u] 的那份是最新的。
if (du + w < d[v])松弛用本次弹出的 du 而不是 d[u]。通过懒删除后两者相等,但用 du 少一次数组访问,也更清楚。
g.resize(n + 1); rg.resize(n + 1);开 n + 1 条空表,下标 1..n 直接用。两个 vector 分别在 main 里 resize,因为点数要到读入之后才知道。
cout << ans << "\n";只有一个整数,末尾换行。不要顺手输出“最少时间是:”这类提示语 —— 那会直接 WA。
这题最容易错的地方
  • 把“回程”当成“去程”:只跑一遍正图,用 distA[i] 乘 2 当答案。有向图里两个方向的距离一般不同,必须建反图。
  • 反图加边时又写成 rg[u].push_back({v, w}):反图变成了正图的复制品,第二遍 Dijkstra 算的还是 d(1, i),答案偏小。
  • 答案用 int:单程最坏 10⁷,累加 10³ 次约 10¹⁰,溢出成负数 → WA。累加与 dist 统一 long long。
  • 把单行道当无向边、每条边加两次:反向边凭空出现,去程和回程的距离都会变小 → WA。
延伸阅读 / 参考