把道路连通块缩点后航线构成 DAG,按拓扑序逐块跑 Dijkstra,块间负权边在目标块处理前即已定值。
OJ: roj
题目 ID: 1503
难度:提高+/省选-
标签:
创建: 2026-09-30 15:06
更新: 2026-10-07 11:01
形式化题目
给定一张有
- 道路:无向边,边权
; - 航线:有向边
,边权 。
额外保证:对任意航线
这一保证用「道路连通块」的语言可以改写为:把每个道路连通块收缩成一个超级点后,所有跨块航线构成的图是一张有向无环图。
暴力解法
思路
图中有负权边,最直接的单源最短路做法是 Bellman-Ford:
dist[S] = 0,其余为 INF
重复 T-1 轮:
对每条边 (a, b, c):
若 dist[a] + c < dist[b],则更新 dist[b]无负权环时它一定收敛到正确答案,并且能自然处理「不可达」——这类点的 dist 始终保持 INF,不会被任何松弛命中。
代码
本解法不单独给出代码文件:它只是用来暴露瓶颈的接力层,正解在渐近复杂度上完全取代了它;
真正的可提交实现在正解的 ### 代码 中(main.py)。
复杂度
每轮要扫过全部
瓶颈
代入最大数据
即便单次操作再便宜,这个量级也远超时间限制。SPFA 的平均表现好得多,但最坏复杂度与 Bellman-Ford 同阶,本题作为最短路专题题,数据里显然有针对它的构造。
瓶颈的根源很清楚:负权边让 Dijkstra 无法直接使用,于是被迫退化成「反复扫描全图」。
正解
关键观察
题面那条看起来很奇怪的保证,正是为负权边量身定做的。把它翻译成「道路连通块」的语言:
- 只走道路时,图被分成若干连通块,块内边权全部非负。
- 把每个块收缩成一个超级点,航线成为超级点之间的有向边。上面的保证说明这张超级点图没有环,即它是一张 DAG。
- 因此超级点存在拓扑序。
下面这张图展示了样例的三层结构:原图 → 按道路缩点 → 航线在超级点上构成 DAG。
graph LR
subgraph L1["原图"]
n1((1)) ---|5| n2((2))
n3((3)) ---|5| n4((4))
n5((5)) ---|10| n6((6))
n3 -.->|-100| n5
n4 -.->|-100| n6
n1 -.->|-10| n3
end
subgraph L2["缩点后的 DAG"]
A((A: 1,2)) -->|-10/-100| B((B: 3,4))
B -->|-100| C((C: 5,6))
end
实线是双向道路,虚线是单向航线。缩点后
思路
Dijkstra 的死穴是「负权边可能让已经出堆的点再次被更新」。拓扑序恰好给出了一个精确的时机:
按拓扑序处理块
时,所有跨块航线 的起点块 都已经处理完毕。所以块 中每个点「从块外拿到」的距离值不可能再变小了。
换句话说,跨块负权边不再构成障碍——它们会在轮到目标块之前全部松弛完毕,形成确定的初值。而块内只剩下非负的道路边,可以直接跑 Dijkstra。
于是算法分成三步:
- 缩点:对每个未访问的城镇做一次只走道路的 BFS,得到一个连通块,记录
comp[v]与块的成员表。 - 拓扑排序:在超级点层面加入所有航线边并统计入度,用 Kahn 算法求拓扑序。
- 逐块 Dijkstra:处理块
时,把块内所有 dist有限的点作为多源起点建堆,跑一次局部 Dijkstra。出堆点时,松弛它的道路边(入堆)和航线边(跨块的只更新 dist,不入本块的堆)。块结束后,把它指向的块入度减 ,归零者入队。
样例的处理过程如下表,可以逐列对照上面的算法步骤:
| 步骤 | 处理的块 | 压入堆的起点 | 结果 |
|---|---|---|---|
| 1 | 块内 dist 全为 INF, |
NO PATH |
|
| 2 | dist[4] = 0(起点) |
出堆 4:航线 dist[6] = -100(跨块,不入堆);道路 dist[3] = 5 入堆;出堆 3:航线 dist[5] = -95(跨块,不入堆) |
|
| 3 | 由 (-100, 6) 与 (-95, 5) |
块内道路 |
最终 dist = [∞, ∞, 5, 0, -95, -100],与样例输出一致。
注意步骤 1:dist 都是 INF,建堆时被 dist[u] < INF 过滤掉,堆空——松弛循环体一次也不会执行,因此不会向
再注意步骤 2 与 3 的分工:4→6 这条负权航线在块 dist[6] 改成一个更小的值,等块
代码
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 15:06
# update_at: 2026-09-30 15:06
import sys
from heapq import heappop, heappush
from collections import deque
INF = 1 << 60 # 不可达哨兵,远大于任何真实最短路(≤ T·10^4)
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
T, R, P = next(data), next(data), next(data)
S = next(data)
# roads:双向非负权边;planes:单向可负权边
roads: list[list[tuple[int, int]]] = [[] for _ in range(T + 1)]
planes: list[list[tuple[int, int]]] = [[] for _ in range(T + 1)]
for _ in range(R):
a, b, c = next(data), next(data), next(data)
roads[a].append((b, c))
roads[b].append((a, c))
for _ in range(P):
a, b, c = next(data), next(data), next(data)
planes[a].append((b, c))
# 第一步:道路双向,所在连通块内部能自由走动 —— 缩成一个点
comp = [-1] * (T + 1)
members: list[list[int]] = [] # 每个块的成员,用于给块内 Dijkstra 建多源堆
for s in range(1, T + 1):
if comp[s] != -1:
continue
comp[s] = len(members)
block = [s]
q = deque([s])
while q:
u = q.popleft()
for v, _ in roads[u]:
if comp[v] == -1:
comp[v] = len(members)
block.append(v)
q.append(v)
members.append(block)
# 第二步:航线必然跨块(块内航线会违反题面保证),缩点后构成 DAG,用入度定拓扑序
dag: list[list[int]] = [[] for _ in members]
indeg = [0] * len(members)
for a in range(1, T + 1):
for b, _ in planes[a]:
dag[comp[a]].append(comp[b])
indeg[comp[b]] += 1
# 第三步:按拓扑序逐块 Dijkstra。处理块 c 时,所有指向它的跨块航线起点
# 都已定值,块内每个点的答案也就此确定,负权不会破坏 Dijkstra 的前提。
dist = [INF] * (T + 1)
dist[S] = 0
topo = deque(c for c in range(len(members)) if indeg[c] == 0)
while topo:
c = topo.popleft()
heap = sorted((dist[u], u) for u in members[c] if dist[u] < INF)
while heap:
d, u = heappop(heap)
if d > dist[u]:
continue
for v, w in planes[u]: # 跨块航线:只是给别的块送初值,不入本块的堆
if d + w < dist[v]:
dist[v] = d + w
for v, w in roads[u]: # 道路权非负,Dijkstra 安全
if d + w < dist[v]:
dist[v] = d + w
heappush(heap, (dist[v], v))
for y in dag[c]:
indeg[y] -= 1
if indeg[y] == 0:
topo.append(y)
sys.stdout.write('\n'.join('NO PATH' if dist[i] == INF else str(dist[i]) for i in range(1, T + 1)))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* 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 05:11
* update_at: 2026-10-05 05:12
*/
// main.cpp:道路连通块缩点 + 拓扑序 + 逐块 Dijkstra(USACO 2011 Jan Gold 道路和航线)。
// 负权航线被限制在块间、由拓扑序消化;块内只有非负道路,Dijkstra 安全。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll, int> Node; // 堆元素:first 为距离,second 为城镇编号
const int MAXT = 25005; // 城镇数上限
const int MAXR = 50005; // 道路数上限
const int MAXP = 50005; // 航线数上限
const ll INF = 1000000000000000000LL; // 不可达哨兵,远大于任何真实最短路绝对值
int T, R, P, S; // T<=2.5e4,R,P<=5e4,用 int 可直接当下标与循环上界
// 道路:无向非负权。链式前向星存双向边,故开 2*MAXR。
int head_road[MAXT], to_road[2 * MAXR], nxt_road[2 * MAXR];
int w_road[2 * MAXR]; // 道路边权 0..10^4,用 int 省内存
int road_cnt;
// 航线:有向可负权。
int head_plane[MAXT], to_plane[MAXP], nxt_plane[MAXP];
int w_plane[MAXP]; // 航线边权 -10^4..10^4,用 int 省内存
int plane_cnt;
int comp[MAXT]; // comp[u] = 城镇 u 所属道路连通块编号(从 0 开始)
int comp_cnt; // 道路连通块总数
vector<int> members[MAXT]; // members[c] = 第 c 个连通块内的城镇列表
vector<int> dag[MAXT]; // dag[c] = 超级点 c 经航线指向的超级点
int indeg[MAXT]; // 超级点入度,用于 Kahn 拓扑排序
ll dist[MAXT]; // dist[u] = 起点 S 到城镇 u 的最短路,INF 表示不可达
// 加一条无向道路(拆成两条有向边)。
void add_road(int u, int v, int w) {
road_cnt++;
to_road[road_cnt] = v;
w_road[road_cnt] = w;
nxt_road[road_cnt] = head_road[u];
head_road[u] = road_cnt;
}
// 加一条单向航线。
void add_plane(int u, int v, int w) {
plane_cnt++;
to_plane[plane_cnt] = v;
w_plane[plane_cnt] = w;
nxt_plane[plane_cnt] = head_plane[u];
head_plane[u] = plane_cnt;
}
void read_input() {
cin >> T >> R >> P >> S;
for (int i = 1; i <= R; i++) {
int a, b, c;
cin >> a >> b >> c;
add_road(a, b, c);
add_road(b, a, c);
}
for (int i = 1; i <= P; i++) {
int a, b, c;
cin >> a >> b >> c;
add_plane(a, b, c);
}
}
// 只走道路做 BFS,把每个城镇归入一个道路连通块。
void build_components() {
int que[MAXT];
for (int s = 1; s <= T; s++) {
if (comp[s] != -1) continue;
int c = comp_cnt;
comp_cnt++;
comp[s] = c;
members[c].push_back(s);
int head = 0, tail = 0;
que[tail] = s;
tail++;
while (head < tail) {
int u = que[head];
head++;
for (int i = head_road[u]; i != 0; i = nxt_road[i]) {
int v = to_road[i];
if (comp[v] == -1) {
comp[v] = c;
members[c].push_back(v);
que[tail] = v;
tail++;
}
}
}
}
}
// 把航线投影到超级点上,得到块间 DAG 与各超级点入度。
void build_dag() {
for (int a = 1; a <= T; a++) {
for (int i = head_plane[a]; i != 0; i = nxt_plane[i]) {
int b = to_plane[i];
if (comp[a] == comp[b]) continue; // 题面保证不存在块内航线,这里防御性跳过
dag[comp[a]].push_back(comp[b]);
indeg[comp[b]]++;
}
}
}
// 按拓扑序逐块 Dijkstra:块内负权已由块外航线送入初值,块内只剩非负道路。
void solve() {
dist[S] = 0;
priority_queue<Node, vector<Node>, greater<Node> > heap;
queue<int> topo;
for (int c = 0; c < comp_cnt; c++) {
if (indeg[c] == 0) topo.push(c);
}
while (!topo.empty()) {
int c = topo.front();
topo.pop();
// 块内所有已定值的城镇作为多源起点,只遍历本块成员,避免扫描全部城镇。
for (size_t k = 0; k < members[c].size(); k++) {
int u = members[c][k];
if (dist[u] < INF) heap.push(Node(dist[u], u));
}
while (!heap.empty()) {
Node cur = heap.top();
heap.pop();
ll d = cur.first;
int u = cur.second;
if (d > dist[u]) continue;
// 跨块航线:只更新目标块的距离,绝不入本块的堆(目标块的点尚未定值)。
for (int i = head_plane[u]; i != 0; i = nxt_plane[i]) {
int v = to_plane[i];
ll nd = d + w_plane[i];
if (nd < dist[v]) dist[v] = nd;
}
// 道路权非负,Dijkstra 安全。
for (int i = head_road[u]; i != 0; i = nxt_road[i]) {
int v = to_road[i];
ll nd = d + w_road[i];
if (nd < dist[v]) {
dist[v] = nd;
heap.push(Node(nd, v));
}
}
}
// 当前块处理完毕,给它在 DAG 中的后继块减少入度。
for (size_t k = 0; k < dag[c].size(); k++) {
int y = dag[c][k];
indeg[y]--;
if (indeg[y] == 0) topo.push(y);
}
}
}
void write_output() {
for (int i = 1; i <= T; i++) {
if (dist[i] == INF) cout << "NO PATH" << '\n';
else cout << dist[i] << '\n';
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
// 只初始化实际用到的 1..T,其余下标永远不会被访问。
for (int i = 1; i <= T; i++) {
comp[i] = -1;
dist[i] = INF;
}
build_components();
build_dag();
solve();
write_output();
return 0;
}复杂度
| 阶段 | 代价 |
|---|---|
| 缩点 BFS | 每条道路访问一次, |
| 建 DAG、统计入度 | 每条航线一次, |
| 拓扑排序 | |
| 逐块 Dijkstra | 每条边最多参与一次堆操作, |
总时间复杂度:
比 Bellman-Ford 的
总结
这道题的关键在于看穿题面保证的图论含义:那条「有航线
实现上有两个容易踩的坑:一是建堆时必须只遍历当前块的成员,若每个块都扫描全部
本解法在全部 16 个官方测试点上通过,最慢 0.114 秒、峰值内存 63.6MB,Python 实现也在限制内(1000ms / 128MB)。