题单 · 习题解析

洛谷 P1266 [BalticOI 2002] 速度限制

★★★ 状态 = 「在哪个路口 + 现在开多快」—— `V = 0` 的边花多久取决于**你是怎么来的**;⚠ 两句藏起来的话各值一个错法(「限速**未知**」≠「不限速」抓 73;「初速为 70」藏在输入格式末行抓 35);★★★ **一个对照档三份自检** —— 抽掉所有 `V = 0` 的边,三个错法**同时**变成能证的精确 0;★★★ 输出的是**路径** ⇒ 裁判 = 验证器 + 独立算的最优时间;⚠ 「仅有一条最快路线」照题面随机是**白送的**(0 / 300 出现并列);⚠⚠ 自己踩的:换 tie-break 只换一维 ⇒ 量出来的假阳性是**假的 0**

原题:洛谷 P1266出自 第 33 章 最短路二:Floyd、Bellman-Ford、SPFA 与负环 的题单题面本地存档:2026-08-31
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P1266,日期见页头。两边不一致时信原站。

题目描述

在这个繁忙的社会中,我们往往不再去选择最短的道路,而是选择最快的路线。 开车时每条道路的限速成为最关键的问题。不幸的是,有一些限速的标志丢失了,因此你无法得知应该开多快。 一种可以辩解的解决方案是,按照原来的速度行驶。你的任务是计算两地间的最快路线。

你将获得一份现代化城市的道路交通信息。为了使问题简化,地图只包括路口和道路。 每条道路是有向的,只连接了两个路口,并且最多只有一块限速标志,位于路的起点。 两地 AB最多只有一条道路从 A 连接到 B。 你可以假设加速能够在瞬间完成并且不会有交通堵塞等情况影响你。当然,你的车速不能超过当前的速度限制。

输入格式

第一行是 3 个整数 NMD2 ≤ N ≤ 1501 ≤ M ≤ 22500)。 N 表示路口的数目,用 0 ~ N−1 标记M 是道路的总数,D 表示你的目的地。

接下来的 M 行,每行描述一条道路,每行有 4 个整数 A0 ≤ A < N),B0 ≤ B < N),V0 ≤ V ≤ 500)和 L1 ≤ L ≤ 500), 这条路是从 AB 的,速度限制是 V,长度为 L如果 V 是 0,表示这条路的限速未知。

如果 V 不为 0,则经过该路的时间 T = L / V。 否则 T = L / V_oldV_old你到达该路口前的速度开始时你位于 0 点,并且速度为 70。

输出格式

输出文件仅一行整数,表示从 0 到 D 经过的城市。

输出的顺序必须按照你经过这些城市的顺序,以 0 开始,以 D 结束仅有一条最快路线。

时限 1 秒,内存 256 MB。

输入输出样例

输入

6 15 1
0 1 25 68
0 2 30 50
0 5 0 101
1 2 70 77
1 3 35 42
2 0 0 22
2 1 40 86
2 3 0 23
2 4 45 40
3 1 64 14
3 5 0 23
4 1 95 8
5 1 0 84
5 2 90 64
5 3 36 40

输出

0 5 2 3 1

0 →(V=0, L=101) 5:限速未知,沿用初速 70,用时 101/705 →(90, 64) 264/902 →(V=0, 23) 3:沿用 90,23/903 →(64, 14) 114/64。 总计约 2.6283。 ★ 注意第三段 2 → 3 那一步:它花多久,完全取决于你是从哪条路开进 2 号路口的 —— 这就是第 ② 步那件事。

1⚠ 先看清楚这道题在问什么:它要的是路径,不是时间

输出格式那一行是「从 0 到 D 经过的城市」—— 所以每个状态都得记一个前驱,最后倒着回溯再翻过来。

p1266Rev.cpp✗ 回溯完忘了翻过来(样例打出 1 3 2 5 0)
// ✗ 错法④:回溯完忘了把路径翻过来
//
// 和算法一点关系都没有 —— 但它是这道题**唯一一个官方样例挡得住**的错法
// (样例答案 `0 5 2 3 1` 倒过来是 `1 3 2 5 0`,一眼就不对)。
#include <bits/stdc++.h>
using namespace std;
const int MAXV = 501;
const double INF = 1e18;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, D;
cin >> n >> m >> D;
vector<vector<array<int, 3>>> g(n);
for (int e = 0; e < m; e++) {
int a, b, v, l;
cin >> a >> b >> v >> l;
g[a].push_back({b, v, l});
}
vector<vector<double>> dist(n, vector<double>(MAXV, INF));
vector<vector<pair<int, int>>> from(n, vector<pair<int, int>>(MAXV, {-1, -1}));
priority_queue<tuple<double, int, int>, vector<tuple<double, int, int>>, greater<>> q;
dist[0][70] = 0;
q.push({0.0, 0, 70});
while (!q.empty()) {
auto [d, u, s] = q.top();
q.pop();
if (d > dist[u][s] + 1e-12) continue;
for (auto [v, lim, len] : g[u]) {
int ns = lim ? lim : s;
double nd = d + (double)len / ns;
if (nd < dist[v][ns] - 1e-12) {
dist[v][ns] = nd;
from[v][ns] = {u, s};
q.push({nd, v, ns});
}
}
}
int best = -1;
for (int s = 1; s < MAXV; s++)
if (dist[D][s] < INF && (best < 0 || dist[D][s] < dist[D][best])) best = s;
vector<int> path;
for (int u = D, s = best; u >= 0; ) {
path.push_back(u);
auto [pu, ps] = from[u][s];
u = pu; s = ps;
}
// ✗ 少了 reverse
for (size_t i = 0; i < path.size(); i++) cout << path[i] << " \n"[i + 1 == path.size()];
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

和算法一点关系都没有,300 轮 300 / 300 全错 —— 而官方样例一眼就挡住了它

2★★★ 关键的一步:状态不是「在哪个路口」,是「在哪个路口 + 现在开多快」

★★★ 一条 V = 0 的路花多久,取决于你是怎么来的

题面:「如果 V 是 0,表示这条路的限速未知……T = L / V_oldV_old 是你到达该路口前的速度。」

⇒ 同一条边,在不同的「来法」下耗时不同 ⇒ 「在哪个路口」这一个数不足以描述你的处境

而速度的取值是有限的:0 ≤ V ≤ 500,而 V = 0 表示「不改速度」 ⇒ 真正可能的速度只有「初速 70」和输入里出现过的那些 V,一律落在 1..500 里。

状态数 150 × 501 = 75 150
转移数 22350 × 5011.1 × 10⁷
顶格本机秒表 0.076 秒(时限 1 秒)

分层图 Dijkstra,和第 32 章 P1073 那道「状态 = 在哪儿 + 贸易做到哪一步」 是同一个动作 —— 那里的第二维是「买卖做到第几步」,这里是「开多快」。

p1266.cpp★ 这一版就能 AC(分层 Dijkstra + 回溯路径,顶格 0.076 秒)
// 洛谷 P1266 [BalticOI 2002] 速度限制 —— ★ 这一版就能 AC
//
// ★★★ 关键的一步:**状态不是「在哪个路口」,是「在哪个路口 + 现在开多快」**。
// 因为一条 V = 0 的路「按原来的速度行驶」—— 走它花多久,取决于你**是怎么来的**。
//
// 速度的取值有限:题面 0 ≤ V ≤ 500,而 V = 0 表示「不改速度」
// ⇒ 真正可能的速度只有「初速 70」和输入里出现过的那些 V,一律落在 1..500 里。
// ⇒ 状态数 150 × 501 = 75150,边数 22500 × 501 —— 分层图 Dijkstra 随便跑。
//
// ⚠ 要输出的是**路径**,不是时间 ⇒ 每个状态记一个前驱,最后倒着回溯再翻过来。
// ⚠ 路口编号是 **0 ~ N−1**,起点是 0 号。
#include <bits/stdc++.h>
using namespace std;
const int MAXV = 501; // 速度 1..500
const double INF = 1e18;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, D;
cin >> n >> m >> D;
vector<vector<array<int, 3>>> g(n); // {到哪儿, 限速 V, 长度 L}
for (int e = 0; e < m; e++) {
int a, b, v, l;
cin >> a >> b >> v >> l;
g[a].push_back({b, v, l});
}
vector<vector<double>> dist(n, vector<double>(MAXV, INF));
vector<vector<pair<int, int>>> from(n, vector<pair<int, int>>(MAXV, {-1, -1}));
priority_queue<tuple<double, int, int>, vector<tuple<double, int, int>>, greater<>> q;
dist[0][70] = 0; // ★ 起点 0 号,初速 70
q.push({0.0, 0, 70});
while (!q.empty()) {
auto [d, u, s] = q.top();
q.pop();
if (d > dist[u][s] + 1e-12) continue;
for (auto [v, lim, len] : g[u]) {
int ns = lim ? lim : s; // ★ V = 0 就沿用当前速度
double nd = d + (double)len / ns;
if (nd < dist[v][ns] - 1e-12) {
dist[v][ns] = nd;
from[v][ns] = {u, s};
q.push({nd, v, ns});
}
}
}
int best = -1;
for (int s = 1; s < MAXV; s++)
if (dist[D][s] < INF && (best < 0 || dist[D][s] < dist[D][best])) best = s;
vector<int> path;
for (int u = D, s = best; u >= 0; ) {
path.push_back(u);
auto [pu, ps] = from[u][s];
u = pu;
s = ps;
}
reverse(path.begin(), path.end()); // ★ 回溯是倒着的,要翻过来
for (size_t i = 0; i < path.size(); i++) cout << path[i] << " \n"[i + 1 == path.size()];
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p1266Node.cpp✗ 状态里只有点,速度记成 spd[u](样例打 0 2 4 1,慢 0.4%)
// ✗ 错法①:状态里只有「在哪个路口」,把速度记成每个点一个值
//
// 这是最自然的第一反应:跑普通 Dijkstra,顺手拿一个 spd[] 记「到达这个点时的速度」。
// ⚠ 它错在**同一个路口可以用不同的速度到达**,而哪一个更划算,
// 取决于后面那段路里有没有 V = 0 的边 —— 「先到」不等于「更好」。
// ⇒ 这就是本章那句「Dijkstra 的贪心要成立,得先把状态定对」的现场
// ([第 32 章 P1073](/sol/p1073/) 那条的同款)。
#include <bits/stdc++.h>
using namespace std;
const double INF = 1e18;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, D;
cin >> n >> m >> D;
vector<vector<array<int, 3>>> g(n);
for (int e = 0; e < m; e++) {
int a, b, v, l;
cin >> a >> b >> v >> l;
g[a].push_back({b, v, l});
}
vector<double> dist(n, INF);
vector<int> spd(n, 0), from(n, -1);
priority_queue<pair<double, int>, vector<pair<double, int>>, greater<>> q;
dist[0] = 0;
spd[0] = 70;
q.push({0.0, 0});
while (!q.empty()) {
auto [d, u] = q.top();
q.pop();
if (d > dist[u] + 1e-12) continue;
for (auto [v, lim, len] : g[u]) {
int ns = lim ? lim : spd[u]; // ✗ 只有一个 spd[u] 可用
double nd = d + (double)len / ns;
if (nd < dist[v] - 1e-12) {
dist[v] = nd;
spd[v] = ns;
from[v] = u;
q.push({nd, v});
}
}
}
vector<int> path;
for (int u = D; u >= 0; u = from[u]) path.push_back(u);
reverse(path.begin(), path.end());
for (size_t i = 0; i < path.size(); i++) cout << path[i] << " \n"[i + 1 == path.size()];
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

顺手写普通 Dijkstra、再拿一个 spd[] 记「到达这个点时的速度」—— 它错在同一个路口可以用不同的速度到达,而哪一种更划算,取决于后面那段路里有没有 V = 0 的边。 「先到」不等于「更好」。

3⚠ 两句藏在题面里的话,各值一个错法

p1266Free.cpp✗ 把 V = 0 读成「不限速」(样例打 0 5 1)
// ✗ 错法②:把 V = 0 读成「这条路不限速」
//
// 题面写的是:「如果 V 是 0,表示这条路的**限速未知**」,而且下一段补了做法 ——
// 「否则 T = L / V_old,V_old 是你到达该路口前的速度。」
// 「未知」不是「不限」。读成不限速的话,这条边的耗时会被算成 0(想开多快开多快),
// 于是它会一头扎进所有 V = 0 的路。
#include <bits/stdc++.h>
using namespace std;
const int MAXV = 501;
const double INF = 1e18;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, D;
cin >> n >> m >> D;
vector<vector<array<int, 3>>> g(n);
for (int e = 0; e < m; e++) {
int a, b, v, l;
cin >> a >> b >> v >> l;
g[a].push_back({b, v, l});
}
vector<vector<double>> dist(n, vector<double>(MAXV, INF));
vector<vector<pair<int, int>>> from(n, vector<pair<int, int>>(MAXV, {-1, -1}));
priority_queue<tuple<double, int, int>, vector<tuple<double, int, int>>, greater<>> q;
dist[0][70] = 0;
q.push({0.0, 0, 70});
while (!q.empty()) {
auto [d, u, s] = q.top();
q.pop();
if (d > dist[u][s] + 1e-12) continue;
for (auto [v, lim, len] : g[u]) {
int ns = lim ? lim : s;
double nd = d + (lim ? (double)len / lim : 0.0); // ✗ V = 0 当成不花时间
if (nd < dist[v][ns] - 1e-12) {
dist[v][ns] = nd;
from[v][ns] = {u, s};
q.push({nd, v, ns});
}
}
}
int best = -1;
for (int s = 1; s < MAXV; s++)
if (dist[D][s] < INF && (best < 0 || dist[D][s] < dist[D][best])) best = s;
vector<int> path;
for (int u = D, s = best; u >= 0; ) {
path.push_back(u);
auto [pu, ps] = from[u][s];
u = pu; s = ps;
}
reverse(path.begin(), path.end());
for (size_t i = 0; i < path.size(); i++) cout << path[i] << " \n"[i + 1 == path.size()];
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

「限速未知」不是「不限速」。读成不限速的话这条边耗时会被算成 0, 于是它一头扎进所有 V = 0 的路。

p1266Init.cpp✗ 忘了初速 70(样例打 0 5 1)
// ✗ 错法③:忘了那句「开始时你位于 0 点,并且**速度为 70**」
//
// 那句话藏在输入格式那一节的最后一行 —— 很容易读过去。
// 这一版把初速当成「随便一个大数」(500,也就是题面允许的最大限速),
// 于是从 0 号出发的 V = 0 那些路会被算得比实际快。
#include <bits/stdc++.h>
using namespace std;
const int MAXV = 501;
const double INF = 1e18;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, D;
cin >> n >> m >> D;
vector<vector<array<int, 3>>> g(n);
for (int e = 0; e < m; e++) {
int a, b, v, l;
cin >> a >> b >> v >> l;
g[a].push_back({b, v, l});
}
vector<vector<double>> dist(n, vector<double>(MAXV, INF));
vector<vector<pair<int, int>>> from(n, vector<pair<int, int>>(MAXV, {-1, -1}));
priority_queue<tuple<double, int, int>, vector<tuple<double, int, int>>, greater<>> q;
dist[0][500] = 0; // ✗ 初速写成 500
q.push({0.0, 0, 500});
while (!q.empty()) {
auto [d, u, s] = q.top();
q.pop();
if (d > dist[u][s] + 1e-12) continue;
for (auto [v, lim, len] : g[u]) {
int ns = lim ? lim : s;
double nd = d + (double)len / ns;
if (nd < dist[v][ns] - 1e-12) {
dist[v][ns] = nd;
from[v][ns] = {u, s};
q.push({nd, v, ns});
}
}
}
int best = -1;
for (int s = 1; s < MAXV; s++)
if (dist[D][s] < INF && (best < 0 || dist[D][s] < dist[D][best])) best = s;
vector<int> path;
for (int u = D, s = best; u >= 0; ) {
path.push_back(u);
auto [pu, ps] = from[u][s];
u = pu; s = ps;
}
reverse(path.begin(), path.end());
for (size_t i = 0; i < path.size(); i++) cout << path[i] << " \n"[i + 1 == path.size()];
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

「开始时你位于 0 点,并且速度为 70」这半句藏在输入格式那一节的最后一行 —— 极容易读过去。

★★★ 一个对照档同时给三个错法做了自检 —— 而且三个 0 都能证

把「V = 0 的边」这件事整个抽掉(生成器档位 2:一条限速未知的路都不给):

300 轮 默认档(三成 V = 0) ★ 八成 V = 0 ★★★ 一条 V = 0 都没有
状态里只有点 6 17 精确的 0
V = 0 当成不花时间 73 109 精确的 0
忘了初速 70 35 82 精确的 0

三个 0 各有各的一行证明,而且都不需要跑程序:

  • 没有 V = 0 的边 ⇒ 每条边的耗时是定死的 ⇒ 速度这一维完全用不上,状态只有点就够了;
  • 没有 V = 0 的边 ⇒ 那个「当成免费」的分支一次都不会走到
  • 初速 70 只在从 0 号出发的 V = 0 边上起作用 ⇒ 没有这种边,写 70 还是写 500 都一样。

⇒ ★★ 这是本书那条老规矩最省事的一次现场: 造一档抽掉那个条件的数据,一次就给三个「精确的 0」做了自检, 同时称出了「限速未知」这件事对三个写法各自的分量。P1439 立的那条。)

4★★★ 输出是一条路径 ⇒ 裁判只能是验证器

★★★ 题面保证「仅有一条最快路线」,可我造的数据不保证

B3644 那条线在这里再走一遍:答案是一个「输出任意一种即可」形状的东西时, 第一件事是写验证器,不是写逐字节对拍。

⚠ 这道题多一层:题面确实保证了唯一(「仅有一条最快路线」), 所以在真题数据上逐字节比是安全的 —— 而我自己造的随机数据没有这个保证

p1266Check.cpp验证器:起点 / 终点 / 每步有边 / 速度规则 / 打出总时间
p1266Brute.cpp参照物:状态图上的 Bellman-Ford,松弛到不动点(只给最优时间)

裁判口径于是是两把尺子拼起来的:验证器说这条路合法且花了多久一份独立的 Bellman-Ford 说最优是多少,两个数相等才算对。

★ 那句「仅有一条最快路线」值多少 —— 照题面随机根本用不上它
300 轮 默认档 八成 V = 0 ★ 长度全 10、限速只取 50 / 100
最快路线不止一条的轮数 0 1 24
两个正确写法逐字节不同(假阳性) 0 1 12

★ 照题面随机时那句保证是白送的:耗时是一串 L / V 的和(L ≤ 500V ≤ 500), 随机数据下两条不同的路撞出完全相同的实数,概率低到 300 轮一次都没有。 ⇒ 得专门把长度压成同一个值、限速压成两个值,才造得出并列。

⚠ 而并列的 24 轮里,两版只有 12 轮真的打出不同的串 —— 另外 12 轮两版的 tie-break 恰好一致第 27 章 P3478 那个现象的又一次)。

p1266Alt.cpp★ 第二个正解:并列时反着挑(用来量假阳性)
⚠⚠ 这儿我自己踩了一个:换 tie-break 也要先问清楚「到底并列在哪一维」

第一版的 p1266Alt.cpp 只换了一处:并列的前驱里取编号最大的。 结果它和正解 300 轮逐字节全同 —— 看着像「这份数据里根本没有并列」。

真因是:并列常常并列在「到终点时开多快」这一维上,节点序列上反而没得选。 正解挑「最优速度」时用的是严格 <(第一个撞上的赢),Alt 也一样 ⇒ 两版从同一个状态起步回溯。

⇒ 把那一处也改成「并列取最大」,假阳性才从 0 变成 12。

★★ 教训:换一个 tie-break 来量「答案唯不唯一」时, 得把状态的每一维都问一遍 —— 只换其中一维,量出来的可能是一个假的 0。

5★ 对拍这一页

300 轮(n 随机 5~8,裁判 = 验证器 + 独立最优时间) 默认档 八成 V = 0 ★ 无 V = 0 ★ 并列档
正解:路径合法且最优 0 坏 0 0 0
第二个正解(并列反着挑) 0 坏 0 0 0
状态里只有点 6 17 0 1
V = 0 当成不花时间 73 109 0 25
忘了初速 70 35 82 0 20
忘了 reverse 300 300 300 300
⚠ 最快路线不止一条的轮数 0 1 0 24
⚠ 官方样例这一次把四个错法「全挡住了」—— 和同一张题单的 P2865 正好两个极端
官方样例挡住了几个
P2865(次短路) 0 / 4(四个全放过)
P1266(这道题) 4 / 4(四个全挡住)

⇒ 「样例是一测就死的过滤器」这条规律的两个极端,出现在同一张题单里。 ★ 而这道题的样例挡得住,是有原因的:它有 15 条边、四段路里两段是 V = 0, 四个错法各自要问的问题,那组数据全都问得出来。 ⇒ 「样例挡不挡得住」的主语是「那组样例的结构」,不是「这个 bug 明不明显」。

6度量程序和生成器

p1266Count.cpp度量程序(300 轮实验整个做在里面:生成器 + 五个版本 + 验证器)
p1266Gen.cpp(五个档位)数据生成器

7一页纸

★★★ 关键的一步 状态 = 「在哪个路口 + 现在开多快」—— V = 0 的边花多久取决于你是怎么来的
★ 规模 状态 150 × 501、转移 1.1 × 10⁷ ⇒ 顶格 0.076 秒
⚠ 两句藏起来的话 「限速未知」≠「不限速」(抓 73);「初速为 70」藏在输入格式末行(抓 35)
★★★ 一个对照档三份自检 抽掉所有 V = 0 的边 ⇒ 三个错法同时变成能证的精确 0
★★★ 输出是路径 题面保证唯一,我造的数据不保证 ⇒ 裁判 = 验证器 + 独立算的最优时间
⚠ 那句保证白送 照题面随机 0 / 300 轮出现并列;压死长度和限速才有 24 / 300
⚠⚠ 自己踩的 换 tie-break 只换了一维 ⇒ 假阳性量出来是假的 0;两维都换才是 12
⚠ 官方样例 四个错法全挡住 —— 和同一张题单的 P2865(全放过)正好两极