0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1266,日期见页头。两边不一致时信原站。
题目描述
在这个繁忙的社会中,我们往往不再去选择最短的道路,而是选择最快的路线。 开车时每条道路的限速成为最关键的问题。不幸的是,有一些限速的标志丢失了,因此你无法得知应该开多快。 一种可以辩解的解决方案是,按照原来的速度行驶。你的任务是计算两地间的最快路线。
你将获得一份现代化城市的道路交通信息。为了使问题简化,地图只包括路口和道路。
每条道路是有向的,只连接了两个路口,并且最多只有一块限速标志,位于路的起点。
两地 A 和 B,最多只有一条道路从 A 连接到 B。
你可以假设加速能够在瞬间完成并且不会有交通堵塞等情况影响你。当然,你的车速不能超过当前的速度限制。
输入格式
第一行是 3 个整数 N、M 和 D(2 ≤ N ≤ 150,1 ≤ M ≤ 22500)。
N 表示路口的数目,用 0 ~ N−1 标记。M 是道路的总数,D 表示你的目的地。
接下来的 M 行,每行描述一条道路,每行有 4 个整数
A(0 ≤ A < N),B(0 ≤ B < N),V(0 ≤ V ≤ 500)和 L(1 ≤ L ≤ 500),
这条路是从 A 到 B 的,速度限制是 V,长度为 L。
如果 V 是 0,表示这条路的限速未知。
如果 V 不为 0,则经过该路的时间 T = L / V。
否则 T = L / V_old,V_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/70;
5 →(90, 64) 2:64/90;2 →(V=0, 23) 3:沿用 90,23/90;3 →(64, 14) 1:14/64。
总计约 2.6283。
★ 注意第三段 2 → 3 那一步:它花多久,完全取决于你是从哪条路开进 2 号路口的 ——
这就是第 ② 步那件事。
1⚠ 先看清楚这道题在问什么:它要的是路径,不是时间
输出格式那一行是「从 0 到 D 经过的城市」——
所以每个状态都得记一个前驱,最后倒着回溯再翻过来。
// ✗ 错法④:回溯完忘了把路径翻过来//// 和算法一点关系都没有 —— 但它是这道题**唯一一个官方样例挡得住**的错法// (样例答案 `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;}点「运行 ▶」看结果
和算法一点关系都没有,300 轮 300 / 300 全错 —— 而官方样例一眼就挡住了它。
2★★★ 关键的一步:状态不是「在哪个路口」,是「在哪个路口 + 现在开多快」
题面:「如果 V 是 0,表示这条路的限速未知……T = L / V_old,
V_old 是你到达该路口前的速度。」
⇒ 同一条边,在不同的「来法」下耗时不同 ⇒ 「在哪个路口」这一个数不足以描述你的处境。
而速度的取值是有限的:0 ≤ V ≤ 500,而 V = 0 表示「不改速度」
⇒ 真正可能的速度只有「初速 70」和输入里出现过的那些 V,一律落在 1..500 里。
| 状态数 | 150 × 501 = 75 150 |
| 转移数 | 22350 × 501 ≈ 1.1 × 10⁷ |
| 顶格本机秒表 | ★ 0.076 秒(时限 1 秒) |
⇒ 分层图 Dijkstra,和第 32 章 P1073 那道「状态 = 在哪儿 + 贸易做到哪一步」 是同一个动作 —— 那里的第二维是「买卖做到第几步」,这里是「开多快」。
// 洛谷 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..500const 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;}点「运行 ▶」看结果
// ✗ 错法①:状态里只有「在哪个路口」,把速度记成每个点一个值//// 这是最自然的第一反应:跑普通 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;}点「运行 ▶」看结果
顺手写普通 Dijkstra、再拿一个 spd[] 记「到达这个点时的速度」——
它错在同一个路口可以用不同的速度到达,而哪一种更划算,取决于后面那段路里有没有 V = 0 的边。
「先到」不等于「更好」。
3⚠ 两句藏在题面里的话,各值一个错法
// ✗ 错法②:把 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;}点「运行 ▶」看结果
「限速未知」不是「不限速」。读成不限速的话这条边耗时会被算成 0,
于是它一头扎进所有 V = 0 的路。
// ✗ 错法③:忘了那句「开始时你位于 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;}点「运行 ▶」看结果
「开始时你位于 0 点,并且速度为 70」这半句藏在输入格式那一节的最后一行 —— 极容易读过去。
把「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 那条线在这里再走一遍:答案是一个「输出任意一种即可」形状的东西时, 第一件事是写验证器,不是写逐字节对拍。
⚠ 这道题多一层:题面确实保证了唯一(「仅有一条最快路线」), 所以在真题数据上逐字节比是安全的 —— 而我自己造的随机数据没有这个保证。
裁判口径于是是两把尺子拼起来的:验证器说这条路合法且花了多久 + 一份独立的 Bellman-Ford 说最优是多少,两个数相等才算对。
| 300 轮 | 默认档 | 八成 V = 0 | ★ 长度全 10、限速只取 50 / 100 |
|---|---|---|---|
| 最快路线不止一条的轮数 | ★ 0 | 1 | ★ 24 |
| 两个正确写法逐字节不同(假阳性) | 0 | 1 | ★ 12 |
★ 照题面随机时那句保证是白送的:耗时是一串 L / V 的和(L ≤ 500、V ≤ 500),
随机数据下两条不同的路撞出完全相同的实数,概率低到 300 轮一次都没有。
⇒ 得专门把长度压成同一个值、限速压成两个值,才造得出并列。
⚠ 而并列的 24 轮里,两版只有 12 轮真的打出不同的串 —— 另外 12 轮两版的 tie-break 恰好一致(第 27 章 P3478 那个现象的又一次)。
第一版的 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(次短路) | ⚠ 0 / 4(四个全放过) |
| P1266(这道题) | ★ 4 / 4(四个全挡住) |
⇒ 「样例是一测就死的过滤器」这条规律的两个极端,出现在同一张题单里。
★ 而这道题的样例挡得住,是有原因的:它有 15 条边、四段路里两段是 V = 0,
四个错法各自要问的问题,那组数据全都问得出来。
⇒ 「样例挡不挡得住」的主语是「那组样例的结构」,不是「这个 bug 明不明显」。
6度量程序和生成器
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(全放过)正好两极 |