题单 · 习题解析

洛谷 P3406 海底高铁

「第 i 段的卡不能坐别的段」把统筹题拆成 N-1 道两选一;★ 而 32 位溢出差 12.7 万倍,样例一字不差

原题:洛谷 P3406出自 第 6 章 前缀和与差分 的题单题面本地存档:2026-08-26
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

该铁路经过 N 个城市,每个城市都有一个站。不过,由于各个城市之间不能协调好, 于是乘车每经过两个相邻的城市之间(方向不限),必须单独购买这一小段的车票。 第 i 段铁路连接了城市 i 和城市 i+11 ≤ 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 ≤ 1000M ≤ 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

样例解释:23 以及 89 买票,其余买卡。 上面那段输出是仓库里的 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 分)

p3406Brute.cpp第 ① 版:一段一段加
样例对。它和正解只差一个差分。
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

★ 题面把它的分数写好了:「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.cpp第 ② 版:差分(能 AC)
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

本机实测(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⚠ 两个坑:一个样例挡得住,一个样例挡不住

p3406NoSwap.cpp坑一:忘了「方向不限」
★ 它在样例上就现形(输出 0)—— 样例第一段旅行就是 3 → 1,往回坐。
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p3406Int.cpp坑二:答案用 32 位存
⚠ 它在样例上和正解**一字不差**(6394)。样例答案还不到 int 上限的百万分之三。
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠ 这道题的溢出不是「差一点」,是差六个数量级
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★★ 对拍:两个错版要的数据完全不同

p3406Gen.cpp生成器:两个档位
参数是「种子 档位」。档位 0 是顺手写法(N, M ≤ 8、票价 ≤ 50)。
// 数据生成器(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 秒
这一页记住三句话
  1. 先找「互不影响的部分」。 「第 i 段的卡不能坐别的段」这一句, 把一道统筹题拆成了 N-1 道两选一 —— 拆开之后剩下的只是模板。
  2. 两个坑,一个样例挡得住(方向不限),一个挡不住(32 位溢出)。 前者只要你真跑了样例就不花钱;后者要靠把数据范围乘一遍。
  3. ★★★ 溢出的触发条件是一条具体的数值线。 生成器造不造得过 2³², 是算出来的,不是感觉出来的 —— 这一页第一版的「顶格档」就差了七倍,300 轮颗粒无收。