0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P3406,日期见页头。两边不一致时信原站。
题目描述
该铁路经过 N 个城市,每个城市都有一个站。不过,由于各个城市之间不能协调好,
于是乘车每经过两个相邻的城市之间(方向不限),必须单独购买这一小段的车票。
第 i 段铁路连接了城市 i 和城市 i+1(1 ≤ i < N)。如果搭乘的比较远,需要购买多张车票。
第 i 段铁路购买纸质单程票需要 Aᵢ 博艾元。
虽然一些事情没有协调好,各段铁路公司也为了方便乘客,推出了 IC 卡。
对于第 i 段铁路,需要花 Cᵢ 博艾元的工本费购买一张 IC 卡,然后乘坐这段铁路一次就只要扣
Bᵢ(Bᵢ < Aᵢ)元。IC 卡可以提前购买,有钱就可以从网上买得到,而不需要亲自去对应的城市购买。
工本费不能退,也不能购买车票。 每张卡都可以充值任意数额。
对于第 i 段铁路的 IC 卡,无法乘坐别的铁路的车。
Uim 现在需要出差,要去 M 个城市,从城市 P₁ 出发分别按照 P₁, P₂, P₃, ⋯, P_M 的顺序
访问各个城市,可能会多次访问一个城市,且相邻访问的城市位置不一定相邻,而且不会是同一个城市。
现在他希望知道,出差结束后,至少会花掉多少的钱,包括购买纸质车票、买卡和充值的总费用。
输入格式
第一行两个整数 N, M。接下来一行,M 个数字,表示 Pᵢ。
接下来 N-1 行,表示第 i 段铁路的 Aᵢ, Bᵢ, Cᵢ。
输出格式:一个整数,表示最少花费。
数据范围
- 对于 30% 数据
M = 2; - 对于另外 30% 数据
N ≤ 1000,M ≤ 1000; - 对于 100% 的数据
M, N ≤ 10⁵,Aᵢ, Bᵢ, Cᵢ ≤ 10⁵。
输入输出样例
输入
9 10 3 1 4 1 5 9 2 6 5 3 200 100 50 300 299 100 500 200 500 345 234 123 100 50 100 600 100 1 450 400 80 2 1 10
输出
6394
样例解释:2 到 3 以及 8 到 9 买票,其余买卡。
上面那段输出是仓库里的 p3406.cpp 真跑出来的。
1★ 关键的一步:这道题其实是 N-1 道互不相干的小题
看着像一道要「统筹安排」的题 —— 买哪些卡、怎么搭配。但题面里有一句把它拆开了:
对于第
i段铁路的 IC 卡,无法乘坐别的铁路的车。
第 i 段的决定只和「第 i 段被走了多少次」有关:
走 cnt 次,全买纸质票: cnt × A
走 cnt 次,买张卡再刷: C + cnt × B
取小的那个。⇒ 于是整道题只剩一个问题:每一段各被走了多少次?
一道看起来要通盘考虑的题,塌成了 N-1 个各自独立的两选一。
★ 这个动作值得单独记住:先找「哪些东西其实互不影响」, 比先想「怎么安排」有用得多。 一旦拆开,剩下的往往是模板。
而「一次旅行 P_j → P_{j+1} 让沿途每一段都 +1」,正是第 6 章后半场
的区间加 —— 差分。
2第 ① 版:沿途一段一段地记(60 分)
// P3406 的第 ① 版:每次旅行,沿途一段一段地记一笔//// ★ 它能拿分:题面写着「对于 30% 数据 M = 2」和「另外 30% 数据 N, M <= 1000」——// 两档加起来 **60 分**,而这一版只比正解少一个差分。//// ⚠ 满数据 N = M = 1e5、每次都从头走到尾 ⇒ 1e10 次自增,跑不完(正文第 ③ 步有秒表)。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; vector<int> p(m); for (int i = 0; i < m; i++) cin >> p[i];
vector<long long> cnt(n + 2, 0); for (int i = 0; i + 1 < m; i++) { int l = p[i], r = p[i + 1]; if (l > r) swap(l, r); for (int j = l; j < r; j++) cnt[j]++; // ← 一段一段地加 }
long long ans = 0; for (int i = 1; i <= n - 1; i++) { long long a, b, c; cin >> a >> b >> c; ans += min(cnt[i] * a, c + cnt[i] * b); } cout << ans << '\n'; return 0;}点「运行 ▶」看结果
★ 题面把它的分数写好了:「30% 数据 M = 2」+「另外 30% 数据 N, M ≤ 1000」= 60 分。
3第 ② 版:差分统计次数 —— 能 AC
int l = p[i], r = p[i + 1];
if (l > r) swap(l, r); // ⚠ 方向不限
d[l] += 1; d[r] -= 1; // 走的是第 l … r-1 段
// P3406 海底高铁 —— 能 AC 的那一版:差分统计次数,再每段各自选便宜的//// ★ 这道题的「关键的一步」是**拆**:// 一次旅行 P_j -> P_{j+1} 看着是一件事,但票是**一段一段买**的,// 而且第 i 段的 IC 卡只能坐第 i 段 ⇒ **N-1 段之间毫无关系**。// 所以只要知道「第 i 段被走了多少次」,每一段就各自是一道两选一的小题://// cnt[i] 次都买纸质票: cnt[i] * A[i]// 买张卡再刷: C[i] + cnt[i] * B[i]// 取小的那个。//// ⇒ 一道看起来要「统筹安排」的题,塌成了 N-1 个互不相干的选择题。//// 而「第 i 段被走了多少次」正是第 6 章后半场那三行:区间加 1 ⇒ 差分。// ⚠ 方向不限:P_j 可能比 P_{j+1} 大,要先 swap 成 [l, r),// 经过的是第 l, l+1, …, r-1 段(共 r-l 段),所以差分打在 d[l] += 1、d[r] -= 1。//// ⚠⚠ 答案必须 long long:cnt <= 1e5、A <= 1e5 ⇒ 一段最多 1e10,// N-1 段加起来 1e15 —— 是 int 上限的**四十七万倍**(不是「差一点」)。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; vector<int> p(m); for (int i = 0; i < m; i++) cin >> p[i];
vector<long long> d(n + 2, 0); for (int i = 0; i + 1 < m; i++) { int l = p[i], r = p[i + 1]; if (l > r) swap(l, r); // ⚠ 方向不限 d[l] += 1; d[r] -= 1; // 走的是第 l .. r-1 段 }
long long ans = 0, cnt = 0; for (int i = 1; i <= n - 1; i++) { cnt += d[i]; // 前缀和还原:第 i 段被走了多少次 long long a, b, c; cin >> a >> b >> c; ans += min(cnt * a, c + cnt * b); // ★ 每段各自选,互不影响 } cout << ans << '\n'; return 0;}点「运行 ▶」看结果
本机实测(B 机:原生 Ubuntu 7.0.0-30 / i5-10210U 8 线程 / 18 GB,2026-08-26,独占;
最坏形状:每趟都在 1 和 N 之间来回,走满 N-1 段):
N = M |
① 一段一段加 | ② 差分 |
|---|---|---|
| 10 000 | 0.04 秒 | 0.00 秒 |
| 30 000 | 0.44 秒 | 0.01 秒 |
| 100 000(满数据) | 5.50 秒 | ★ 0.03 秒 |
4⚠ 两个坑:一个样例挡得住,一个样例挡不住
// P3406 的错法演示:忘了「方向不限」//// 题面写着「乘车每经过两个相邻的城市之间(**方向不限**)」——// 也就是 P_j 可能**大于** P_{j+1}(往回坐)。// 少了那句 swap,区间就成了「左端比右端大」,这一趟旅行等于一段都没记。//// ★ 这一处**样例挡得住**:样例的第一段旅行就是 3 -> 1(往回坐)。// ⇒ 又一次「先跑样例、逐字节比」的价值 —— 前面几页里样例挡不住的坑出现过太多次,// 以至于容易忘了:**挡得住的那些,只要你真跑了就一分钱不花。**
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; vector<int> p(m); for (int i = 0; i < m; i++) cin >> p[i];
vector<long long> d(n + 2, 0); for (int i = 0; i + 1 < m; i++) { int l = p[i], r = p[i + 1]; // ⚠ 少了 if (l > r) swap(l, r); d[l] += 1; d[r] -= 1; }
long long ans = 0, cnt = 0; for (int i = 1; i <= n - 1; i++) { cnt += d[i]; long long a, b, c; cin >> a >> b >> c; ans += min(cnt * a, c + cnt * b); } cout << ans << '\n'; return 0;}点「运行 ▶」看结果
// P3406 的错法演示:答案用 32 位存//// ★ 为什么这份用的是 unsigned 而不是 int(第 45 章那一课):// int 溢出在 C++ 里是**未定义行为** —— 它可能回绕、可能被 -O2 优化成别的样子,// 换台机器结果就变,**这种数字写不进教材**。// 换成 unsigned,「超过 2³² 就从头绕」是标准写死的,谁跑都一样。// ⇒ 演示错误写法时,要把那个错**钉成可复现的**。//// ⚠ 除了 ans 和 cnt 的类型,它和正解一模一样。// 而**样例挡不住它**:样例答案 6394,连 int 的零头都不到。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; vector<int> p(m); for (int i = 0; i < m; i++) cin >> p[i];
vector<int> d(n + 2, 0); for (int i = 0; i + 1 < m; i++) { int l = p[i], r = p[i + 1]; if (l > r) swap(l, r); d[l] += 1; d[r] -= 1; }
unsigned ans = 0; // ⚠ 32 位(学生真写的是 int,这里见文件头) unsigned cnt = 0; for (int i = 1; i <= n - 1; i++) { cnt += (unsigned)d[i]; unsigned a, b, c; cin >> a >> b >> c; ans += min(cnt * a, c + cnt * b); // ⚠ 这里的乘法也是 32 位的 } cout << ans << '\n'; return 0;}点「运行 ▶」看结果
cnt ≤ M = 10⁵,A ≤ 10⁵ ⇒ 一段最多 10¹⁰
N-1 段加起来 ⇒ 最多 10¹⁵
int 的上限 2 147 483 647 ≈ 2.1×10⁹本机在满数据(N = M = 10⁵ 最坏形状)上实测:
| 值 | |
|---|---|
正解(long long) |
499 203 615 313 925 |
| 32 位那版 | 3 936 096 338 |
| 差 | ★ 12.7 万倍 |
★ 和同一章的 P8218 正好凑成一对:那道题的上界是 10⁹,
int 恰好装得下(余量 2 倍);而这道题上面那个真实答案,是 int 上限的 23.2 万倍。
⇒ 两道题都要把数据范围乘一遍才知道自己在哪边 —— 差别不在题目难度,在乘法。
⚠ 顺带一句写法上的:这份演示用的是 unsigned 而不是 int。
因为 int 溢出是未定义行为,换台机器、换个优化选项结果就变,
那种数字写不进教材;unsigned 的回绕是标准写死的,谁跑都一样
(第 45 章那一课)。
5★★ 对拍:两个错版要的数据完全不同
// 数据生成器(P3406 对拍用):`./p3406Gen <seed> [level]`//// level 0(默认)顺手写法:N <= 8、M <= 8,A/B/C 都是 1..50 的小数// level 1 ★ **把答案顶到 32 位装不下**:N、M 拉到几百,A/C 取 1e5 附近、// B 取 A 的一半以上(让卡也省不下多少),旅行都是「从头走到尾」//// ★★ 两个档位抓的是**不同的** bug(正文第 ⑤ 步那张表):// 忘了 swap 那版,档位 0 一抓一个准(随机的 P 有一半是往回走的);// 而 32 位溢出那版,**档位 0 一次都抓不到** —— 小数值算出来的答案只有几千,// 离 2³² 差着六个数量级。⇒ 生成器的**取值范围**不覆盖,轮数再多也没用// (第 5 章 P1042 那条:先写下触发条件,再问生成器造不造得出)。
#include <bits/stdc++.h>using namespace std;
static mt19937 rng;static int ri(int lo, int hi) { return lo + (int)(rng() % (unsigned)(hi - lo + 1)); }
int main(int argc, char** argv) { unsigned seed = (argc > 1) ? (unsigned)atoi(argv[1]) : 1; int level = (argc > 2) ? atoi(argv[2]) : 0; rng.seed(seed); // ⚠ 这两个数是**算过**的:每段的花费约 m×B ≈ 400×7万 = 2.8e7, // 乘上 n ≈ 400 段就是 1e10 —— 稳稳超过 32 位能装的 4.29e9。 // (第一版写的是 n, m <= 80,答案只到 6e8,**一次都没抓到**,白跑了 300 轮。) int n = (level == 1) ? ri(300, 400) : ri(2, 8); int m = (level == 1) ? 400 : ri(2, 8); printf("%d %d\n", n, m); for (int i = 0; i < m; i++) { int v = (level == 1) ? ((i & 1) ? n : 1) : ri(1, n); // 顶格档:来回走全程 printf("%d%c", v, i == m - 1 ? '\n' : ' '); } for (int i = 1; i <= n - 1; i++) { int a, b, c; if (level == 1) { a = ri(90000, 100000); b = ri(a / 2, a - 1); c = ri(90000, 100000); } else { a = ri(2, 50); b = ri(1, a - 1); c = ri(1, 50); } printf("%d %d %d\n", a, b, c); } return 0;}点「运行 ▶」看结果
本机实测(check:viz 每档 300 轮,暴力当标准答案):
| 生成器 | 忘了 swap |
32 位溢出 |
|---|---|---|
档位 0:顺手小数据(N, M ≤ 8,票价 ≤ 50) |
246 / 300 | ★ 0 / 300 |
档位 1:把答案顶过 2³²(N, M ≈ 400,票价 ≈ 10⁵) |
300 / 300 | ★ 300 / 300 |
这一页做的时候真踩了一次:档位 1 第一版写的是 N, M ≤ 80、票价取 10⁵ ——
票价确实顶格了,可算出来的答案只有 6×10⁸,
离 32 位那条线(4.29×10⁹)还差七倍。⇒ 300 轮一次都没抓到。
改法不是「再随机随机」,是算一遍:
每段花费约 M × B ≈ 400 × 7万 = 2.8×10⁷,乘上 N ≈ 400 段就是 10¹⁰ —— 稳稳过线。
把 N, M 从 80 改成 400 之后,300 / 300。
⇒ 一句话:溢出类的 bug,触发条件是一条具体的数值线(这里是 2³²);
生成器要不要改,是一道算术题,不是手感问题。
(第 5 章 P1042 那条是「行数够不够」,这一条是「数值够不够大」——
同一件事的两种长相。)
6四个版本并排
| 版本 | 差在哪 | 样例 | 满数据 | 能过吗 |
|---|---|---|---|---|
① p3406Brute |
沿途一段一段加 | ✓ | 5.50 秒 | ★ 60 分 |
② p3406NoSwap |
少一句 swap |
✗ 输出 0 | — | ✗ |
③ p3406Int |
答案用 32 位 | ✓ 一字不差 | ✗ 差 12.7 万倍 | ✗ |
④ p3406 |
—— | ✓ | ★ 0.03 秒 | ★ 能 |
- ★ 先找「互不影响的部分」。 「第
i段的卡不能坐别的段」这一句, 把一道统筹题拆成了N-1道两选一 —— 拆开之后剩下的只是模板。 - ⚠ 两个坑,一个样例挡得住(方向不限),一个挡不住(32 位溢出)。 前者只要你真跑了样例就不花钱;后者要靠把数据范围乘一遍。
- ★★★ 溢出的触发条件是一条具体的数值线。 生成器造不造得过
2³², 是算出来的,不是感觉出来的 —— 这一页第一版的「顶格档」就差了七倍,300 轮颗粒无收。