最短路(Spfa)

GitHub跳转原题关系图返回列表

边权全正的无向图单源最短路模板:堆优化 Dijkstra 按距离从小到大定型每个点,惰性删除过期堆条目,N 第一次出堆即得答案,复杂度 O(M log M)。

OJ: roj

题目 ID: 1382

难度:普及+/提高-

标签:图论最短路Dijkstra堆python

创建: 2026-09-30 07:57

更新: 2026-10-07 11:01

后置题目
roj 3597文化之旅B 的核心引擎复用了 A 教的堆优化 Dijkstra 与「首次弹出即定型、命中终点提前返回」这一步:A 用来定型单个点,B 把它搬到 (国家,已学文化集合) 状态图上,首次弹出 u=T 的状态即得答案,再叠加状压建模、A* 启发与 -1 预检。roj 3209「Sightseeing」 观光B 的 main.py 先跑一遍与 A 同款的堆优化 Dijkstra(shortest_from 里保留 `if d > dist[u]: continue` 的惰性删除)求出 base 数组,再用「首次弹出/到达即定型」这一条把每个状态 (v,k) 的总长与条数一次定死并向后继分发;A 只教单源最短路模板,B 在其上叠加超额只有 0/1 的证明、两层状态建模与 ways 条数累加。roj 3070「Full Tank」 装满的油箱B 的 Dial 桶队列沿用 A 的「首次弹出即定型」关键观察:B 证明油钱单调不减、故首次取出 (E,·) 即最优,只是把 A 的二叉堆换成桶队列并把惰性删除搬到 dist[u][f]!=idxroj 3071第K短路B 的 h_values 直接在反图上复用 A 教的堆优化 Dijkstra(含惰性删除过期堆条目)求到终点最短路,再外叠 A* 才是新增难度。roj 2063usaco-4.1.3 篱笆回路B 的最小环枚举把 A 教的堆优化 Dijkstra 当子程序原样复用:main.py 的 detour 里同样是「松弛成功就压堆、弹出时 if d > dist[x] 跳过过期条目、命中目标点立即返回」,只是加了 banned 禁边参数,A 只跑一次单源最短路,B 把它对每条篱笆各跑一次再取 w+dist 最小值,并另行叠加「相接集合当端点编号」与「区域周长=最小环」两步。luogu P9724[EC Final 2022] Chase GameB 正解的第一步就是在每步伤害 d-dist(k,v) 构成的非负权图上复用 A 教的堆优化 Dijkstra(main.cpp 同样是 priority_queue + vis 惰性删除的定型模板)求出 dist_shou,再在其外叠加「以第一次传送分两段、等差数列 O(1) 结算」的额外流程。luogu P1875佳佳的魔法药水【数据有误】B 复用 A 教的堆优化 Dijkstra「按距离从小到大定型」单调序:A 证明候选距离最小者一次定型,B 照此先按最小成本弹出定型药水(main.py 同样用小根堆加 current!=distance 跳过过期条目),再按成本从小到大统计配方超边的松弛与最优方案数,额外叠加的只是 A+B->C 超边建模与方案计数这一层 A 未讲的流程。luogu P3403跳楼机B 的余数图里每个余数的最早可达楼层 dist[r] 正是由 A 教的「弹出即定型 + 惰性删除」Dijkstra 一步步定出的(B 的 main.py 也原样保留了 `current != distance[residue]` 的过期条目跳过),B 只是在此之上叠加取模建图与 (h-dist[r])//base+1 计数的新流程。luogu P4568[JLOI2011] 飞行路线B 把 A 教的堆优化 Dijkstra 骨架整体搬来做起点(文章明说“直接运行 Dijkstra”,main.py 里 current != distance[used][node] 跳过过期条目正是 A 教的惰性删除),只是把状态从一维 dist 扩成 (city,used) 分层图并叠加“花一次机会以 0 代价进入下一层”的新流程。luogu P2939[USACO09FEB] Revamping Trails GB 直接复用 A 的判据“边权全正/非负 ⟹ 用堆优化 Dijkstra 定型”,把同一算法原样套到 (点, 已用改造数) 状态图上,只额外叠加了改造机会这一状态维与 0 权转移luogu P4822[BJWC2012] 冻结B 的 main.cpp 把 A 教的堆优化 Dijkstra(弹出即定型 + 用 done 惰性删除过期堆条目)原样搬到 (城市, 已用卡数) 状态图上,只把一维 done 扩成 vis[u][used],A 未教的分层状态建模是 B 叠加的新流程,故 A 是 B 的前置台阶。luogu P1186玛丽卡B 的整个解法就是把 A 教的堆优化 Dijkstra 模板当作子过程反复调用:第一次跑出 1 到 N 的最短路并记录前驱点/前驱边,回溯出路径边集后逐条禁用重跑同一 Dijkstra 取最大值;A 负责的 Dijkstra 定型与惰性删除是 B 每一步实际执行的一步,B 在其上叠加的是路径恢复与枚举删边的额外流程。luogu P2176[USACO11DEC] RoadBlock S / [USACO14FEB] Roadblock G/SB 把 A 教的堆优化 Dijkstra 当黑盒模板调用:第一次跑时在同一次松弛里顺带记录前驱边以回溯最短路,随后对这条路上的每条边重跑同一模板取增量

形式化题目

给定 NN 个点、MM 条边的带权无向图(N⩽100000N \leqslant 100000,M⩽500000M \leqslant 500000),每条边权 cic_i 是不超过 10001000 的正整数。图中可能有重边和自环,数据保证 11 与 NN 连通。求点 11 到点 NN 的最短路径长度,只输出距离值。

样例是一张 4 个点 4 条边的图,加粗的两条边构成最优路线 1→2→41 \to 2 \to 4:

flowchart LR
  s1["1"] ---|"1"| s2["2"]
  s2 ---|"1"| s3["3"]
  s3 ---|"1"| s4["4"]
  s2 ---|"1"| s4
  linkStyle 0,3 stroke:#c0392b,stroke-width:3px

观察这张图:22 到 44 之间既有长度 11 的直接边,也有绕经 33、总长 22 的路线。最短路要求把一个点对之间所有可能路线放在一起比较总长,而不是只盯住某条边或某条路——这正是最短路算法要替我们做的全局比较。

正解

思路

从朴素做法出发。 最短路的原始定义是松弛操作:若经过 uu 能把到 vv 的距离改得更小,就更新

dist[v]=min⁡(dist[v], dist[u]+w(u,v)).\mathrm{dist}[v] = \min\bigl(\mathrm{dist}[v],\ \mathrm{dist}[u] + w(u,v)\bigr).

从 dist[1]=0\mathrm{dist}[1] = 0、其余为 ∞\infty 出发,反复扫描全部边做松弛直到不再变化,就是 Bellman-Ford 算法,复杂度 O(NM)O(NM)。本题 N⩽105N \leqslant 10^5、M⩽5×105M \leqslant 5\times10^5,NMNM 高达 5×10105\times10^{10},连时限的百分之一都跑不完。

瓶颈在哪。 每一轮都无差别扫描所有边,而绝大多数边在当时根本不可能引起有效松弛。改进方向自然有两种:要么"只扫刚变小的点",要么"给该轮到谁定序"。

  • 路线一:把被松弛成功的点塞进队列,下一轮只从它们出发扩展,这就是 SPFA(本题官方标题与标程的做法)。平均很快,但最坏复杂度仍是 O(NM)O(NM),没有任何保证,Python 实现常数又大。
  • 路线二:本题边权全为正,这个条件允许更强的贪心——当前候选距离最小的未定型点,它的距离已经不可能再被别的路改小,可以一次性"定型"。用小根堆维护"谁的候选距离最小",就是堆优化 Dijkstra,复杂度 O(Mlog⁡M)O(M\log M),有硬保证。

"第一次弹出堆即定型"为什么对?归纳来看:所有边权为正,任何还没定型的点若想把 uu 改得更小,路径必须先经过某个距离不小于 dd 的点再走正权边,只会更大。所以弹出 (d,u)(d, u) 时 dd 就是 uu 的最终最短距离。

用样例走一遍流程(点按编号 1..41..4):

步骤 弹出 (d,u)(d, u) 动作 dist\mathrm{dist} 状态 [1,2,3,4][1,2,3,4]
初始 — dist[1]=0\mathrm{dist}[1]=0 入堆 [0,∞,∞,∞][0, \infty, \infty, \infty]
1 (0,1)(0, 1) 松弛 1→21\to2:0+1<∞0+1<\infty [0,1,∞,∞][0, 1, \infty, \infty]
2 (1,2)(1, 2) 松弛 2→32\to3、2→42\to4 均成功 [0,1,2,2][0, 1, 2, 2]
3 (2,3)(2, 3) 松弛 3→43\to4:2+1=3>22+1=3 > 2,失败 [0,1,2,2][0, 1, 2, 2]
4 (2,4)(2, 4) u=Nu=N,直接返回 2 答案 22

表格里有两个值得注意的细节:第 3 步经过 33 绕路到 44 反而更贵,松弛自动淘汰了它;第 4 步 44 被弹出时答案就已确定,不用等堆空——"命中终点提前返回"是完全安全的。

实现上的三个要点。

  1. 惰性删除:一个点每被松弛成功一次就压一次堆,堆里会同时存在同一点的多个条目。弹出时若 d>dist[u]d > \mathrm{dist}[u],说明这是过期条目,直接跳过。这比手写"可删除堆"简单得多,也是 Python 里最省事的标准写法。
  2. 重边与自环不用特判:无向边在两端各存一条即可;自环因 c>0c > 0 永远满足 dist[u]+c>dist[u]\mathrm{dist}[u]+c > \mathrm{dist}[u],松弛条件"严格小于"让它自动失效;重边中较大的一条会在比较中自然落败。
  3. 读入规模:M=5×105M = 5\times10^5 时输入约 7.8MB,必须 sys.stdin.buffer.read() 一次读完再按空白切分,逐行 input() 会先死在读入上。

代码

Python 版:

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 08:00
# update_at: 2026-09-30 08:00

import sys
from heapq import heappop, heappush

INF = 1 << 60  # 距离哨兵:大于任何可能的最短路(最长不超过 n*w ≤ 1e5 * 1000)


def dijkstra(n: int, adj: list[list[tuple[int, int]]]) -> int:
    """返回 1 到 n 的最短距离:权值全为正,堆优化 Dijkstra 按距离从小到大敲定每个点。"""
    dist = [INF] * (n + 1)
    dist[1] = 0
    heap = [(0, 1)]  # (候选距离, 点):堆顶是当前候选距离最小的点
    while heap:
        d, u = heappop(heap)
        if u == n:  # n 第一次出堆时它的候选距离已不可能再变小
            return d
        if d > dist[u]:  # 过期条目:u 后来又被更小的距离松弛过,跳过
            continue
        for v, w in adj[u]:
            nd = d + w
            if nd < dist[v]:
                dist[v] = nd
                heappush(heap, (nd, v))
    return dist[n]  # 题面保证 1 到 n 连通,正常运行到不了这里


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    n, m = next(data), next(data)

    adj: list[list[tuple[int, int]]] = [[] for _ in range(n + 1)]
    for _ in range(m):
        a, b, c = next(data), next(data), next(data)  # 无向边 a—b,长 c
        adj[a].append((b, c))
        adj[b].append((a, c))  # 重边、自环照常存,松弛时自然被淘汰

    print(dijkstra(n, adj))


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-05 00:11
 * update_at: 2026-10-05 00:11
 */

// 1382 最短路:边权全为正的无向图单源最短路,堆优化 Dijkstra。
// N 第一次出堆时它的候选距离已不可能再变小,直接得到答案。

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

const int MAXN = 100005;
const int MAXM = 500005;

typedef long long ll;

int n, m;
int head[MAXN], nxt[MAXM * 2], to[MAXM * 2], edge_cnt; // 链式前向星,无向边存两条
ll weight[MAXM * 2]; // 边权用 ll,最短路累加不会溢出
ll dist_[MAXN]; // dist_[u] 表示当前 1 到 u 的候选最短距离
bool done[MAXN]; // done[u] 为 true 表示 u 已定型(第一次出堆)

priority_queue< pair<ll, int>, vector< pair<ll, int> >, greater< pair<ll, int> > > heap; // 小根堆:(候选距离, 点)

// 加一条 u -> v、长 w 的边。
void add_edge(int u, int v, int w) {
    edge_cnt++;
    to[edge_cnt] = v;
    weight[edge_cnt] = w;
    nxt[edge_cnt] = head[u];
    head[u] = edge_cnt;
}

// 堆优化 Dijkstra:按距离从小到大定型每个点,返回 1 到 n 的最短距离。
// 重边、自环不用特判:松弛条件是严格小于,较大的重边自然落败,正权自环永远松弛失败。
ll dijkstra() {
    const ll INF = 1e18;
    for (int i = 1; i <= n; i++) {
        dist_[i] = INF;
        done[i] = false;
    }
    dist_[1] = 0;
    while (!heap.empty()) heap.pop(); // 清空全局堆
    heap.push(make_pair(0LL, 1));

    while (!heap.empty()) {
        int u = heap.top().second;
        ll d = heap.top().first;
        heap.pop();
        if (done[u]) continue; // 过期条目:u 已被更小的距离定型过,跳过
        done[u] = true;
        if (u == n) return d; // n 第一次出堆,答案已确定,提前返回
        for (int i = head[u]; i != 0; i = nxt[i]) {
            int v = to[i];
            ll nd = d + weight[i];
            if (nd < dist_[v]) { // 松弛成功才压堆,堆中可能同点多条目,靠 done 惰性删除
                dist_[v] = nd;
                heap.push(make_pair(nd, v));
            }
        }
    }
    return dist_[n]; // 题面保证 1 与 n 连通,正常运行到不了这里
}

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

    cin >> n >> m;
    edge_cnt = 0;
    for (int i = 1; i <= m; i++) {
        int a, b, c;
        cin >> a >> b >> c;
        add_edge(a, b, c); // 无向边两端各存一条
        add_edge(b, a, c);
    }

    cout << dijkstra() << "\n";

    return 0;
}

复杂度

  • 时间复杂度:每次松弛成功压一次堆,无向边两个方向的松弛总次数不超过 2M2M,堆操作 O(log⁡M)O(\log M),总计 O((N+M)log⁡M)O\bigl((N+M)\log M\bigr),与 C++ 标准堆优化最短路解法同阶。实测最大数据(N=105N=10^5,M=5×105M=5\times10^5)本地运行约 0.45s。
  • 空间复杂度:邻接表 O(M)O(M)、dist 与堆 O(N+M)O(N+M),合计 O(N+M)O(N+M)。Python 对象开销大,实测峰值约 238MB,会超出本题 128MB 内存限制;这是 Python 解法的已知代价(C++ 同算法约 20MB),需要严格卡内存时应换 C++ 或改用扁平数组存边。

总结

  • 本题是单源最路模板:边权全正 ⟹ 堆优化 Dijkstra 成立;只求 1→N1\to N 的距离值 ⟹ NN 第一次出堆即可返回。
  • 推导主线:Bellman-Ford 的 O(NM)O(NM) 全量扫描是瓶颈 ⟹ SPFA"只扫刚变小的点"(官方标程路线,最坏无保证)⟹ Dijkstra"按距离从小到大定型"O(Mlog⁡M)O(M\log M)(本文采用的路线,复杂度有硬保证)。
  • 三个实现要点可以直接迁移到任何最短路题:惰性删除过期堆条目、重边自环不特判(严格小于的松弛条件兜底)、大数据一次 read() 再切分。