最后一节,一场 120 分钟的模拟赛:5 道题,难度都在洛谷黄题到蓝题之间。这里不出现任何新算法——60 节的内容全部在第 1~59 节讲过。这一节只做两件事:把 60 节压成一张 12 行的覆盖表,让你知道每个知识点会以什么形式出现在题面里;再用 6 段代码把它们钉住。
先把结论摆出来:这 5 题的正解加起来只有 6 段模板——差分(9 行)、二分答案(6 行)、堆优化 Dijkstra(20 行)、树状数组(10 行)、LIS 的二分替换(6 行)、KMP 的 nxt 递推(5 行)。剩下 54 节不是没用,而是以「判断」的形式出现:看到 n = 5×105 就知道 O(n2) 必死,看到「最大值最小」就知道要二分,看到 231 就知道中间量可能溢出 int。
| 组 | 节 | 核心工具 | 在本套里的落点 |
|---|---|---|---|
| 一 · 起步 | 1–5 | cin/cout、for、数组、整数范围 | 六段代码的读入输出;B 的 5×105 数组;D 的 231 边界 |
| 二 · 数组两种用法 | 6–10 | 桶计数、二维数组、字符串 | A 的费用数组与邻接表;B 的离散化数组;D 的 106 标记数组 |
| 三 · 函数与排序 | 11–15 | sort/cmp、unique、离散化 | B 的排序去重 + lower_bound;C 的排列映射 |
| 四 · 贪心与前缀和 | 16–20 | 交换论证、前缀和、差分 | 例题的差分 + 贪心;B 的树状数组就是「动态前缀和」 |
| 五 · 递归与搜索 | 21–25 | 递归出口、回溯、复杂度估算 | B 的另一解:归并排序求逆序对(递归分治) |
| 六 · BFS 与二分 | 26–30 | BFS、二分查找、二分答案 | A 的 check 单调性 + 二分;C 的 LIS 二分替换 |
| 七 · DP | 31–35 | 状态定义、转移、边界 | C 的 LIS 是 DP 的二分优化形态 |
| 八 · 数论 | 36–40 | 埃氏筛、gcd、快速幂、组合数 | D 的埃氏筛 + 区间筛 |
| 九 · 图论 | 41–45 | 邻接表、Dijkstra、MST、拓扑 | A 的建图与最短路 |
| 十 · 进阶数据结构 | 46–50 | 树状数组、单调队列、堆 | B 的 bit;A 的 priority_queue |
| 十一 · 字符串与倍增 | 51–55 | 哈希、Trie、KMP、ST 表 | E 的前缀函数与匹配 |
| 十二 · 进阶专题 | 56–60 | 分治、根号分治、博弈、几何 | B 的归并分治;D 的筛法复杂度 √R log log R |
五题覆盖不了每个角落。概率与期望(39)、根号分治与莫队(57)、博弈(58)、计算几何(59)这四块本套没有设题,考的是结论而不是代码:期望的线性性、块大小取 √n、SG 值异或、叉积判方向。考前各花 10 分钟扫一遍结论,不要在这个时候学新东西。
| 标记 | 题号 | 题目 | 主要考点 | 难度 | 建议用时 |
|---|---|---|---|---|---|
| A | P1462 | 通往奥格瑞玛的道路 | 二分答案 + 堆优化 Dijkstra | 提高- | 20 分钟 |
| B | P1908 | 逆序对 | 普及/提高- | 提高- | 20 分钟 |
| C | P1439 | 【模板】最长公共子序列 | 排列映射 + LIS 二分 | 提高- | 20 分钟 |
| D | P1835 | 素数密度 | 埃氏筛 + 区间筛 | 提高- | 20 分钟 |
| E | P3375 | 【模板】KMP 字符串匹配 | 普及/提高- | 提高- | 25 分钟 |
下面四张卡不是新知识,是把这 5 题反复用到的四个动作各默写一遍。赛场上默写不出来的模板,等于没学过:先能一次写对,再谈限时。
| 组成 | 含义 / 格式要求 |
|---|---|
d 的定义 | 相邻两项之差。约定 a0 = 0,于是 d1 = a1。 |
为什么有用 | 「对一段区间整体加 x」在差分里只改两个位置:d[l] += x、d[r+1] -= x。区间操作从 O(长度) 降到 O(1)。 |
差分与前缀和互逆 | 对 d 求一次前缀和就回到 a。所以要「区间加 + 区间查」时,靠的是树状数组维护 d 的前缀和(第 46 节)。 |
相邻比较 | a[i] − a[i−1] 就是 d[i]。凡题面问「比前一段多多少」「新开了几段」,答案往往是 Σ max(0, d[i])。 |
a[0] = 0 不能省 | 第一段的差分 d1 = a1 − a0。漏掉这个约定,第一项就没有基准,例题的天数会少算。 |
下标统一 | 差分可以 0 起也可以 1 起,但同一个程序里必须一致;混用是这类题最常见的 WA。 |
int a[6] = {4, 3, 2, 5, 3, 5};
int pre = 0, ans = 0;
for (int i = 0; i < 6; ++i) {
int d = a[i] - pre; // 差分:比前一段多(少)多少
if (d > 0) ans += d; // 只累加「变深」的部分
pre = a[i]; // 更新基准
}
// 差分序列: 4 -1 -1 3 -2 2,求和 = 5 = 最后一段的深度
// ans = 4 + 3 + 2 = 9差分只压缩「区间整体加减」。如果还要随时查询区间和,单靠差分不行,得配树状数组或线段树。
答案可能到 109,累加统一用 long long,不要每道题都去算边界。
| 组成 | 含义 / 格式要求 |
|---|---|
前提:单调性 | lim 可行,则更大的 lim 一定也可行。A 题里 lim 是「城市费用上界」,放宽上界只会多出能走的城市,最短路只会变短,所以可行域是一个后缀。 |
lo / hi 是什么 | 候选集的下标,不是答案本身。A 题里候选集是「所有城市费用排序去重后的数组」。 |
mid | (lo + hi) / 2 向下取整。担心 lo + hi 溢出时写 lo + (hi - lo) / 2;本题下标 ≤ 104,不必。 |
check 的代价 | 每次 check 跑一遍 Dijkstra,O(m log n)。log n ≈ 14 次,总共约 14 × 5×104 × 14 ≈ 107。 |
退出后再判一次 | 不可行时二分仍会停在最后一位。必须先 check 候选集的最大值,不可行就直接输出无解标记。 |
为什么先排序去重 | 答案只可能是某个城市的费用。在 n 个候选值上二分比在 [0, 109] 上二分少一半次数,也避免「答案不是任何城市费用」的尴尬。 |
// val[0..m-1] 已升序去重;check(i) 表示「用 val[i] 作上界可行」
int lo = 0, hi = m - 1;
while (lo < hi) {
int mid = (lo + hi) / 2;
if (check(mid)) hi = mid;
else lo = mid + 1;
}
// 循环结束 lo == hi,val[lo] 就是答案check 的边界(写 ≤ 还是 <)直接决定答案差一格。写完先用样例把「刚好可行」的那个值手动验一遍。
二分的是候选值集合的下标,不是值域。搞混了就会在 [0, 109] 上多跑十几次 check。
| 组成 | 含义 / 格式要求 |
|---|---|
为什么要离散化 | B 题 ai 到 109,而树状数组的下标必须落在 [1, n]。把「值」换成「它在所有值里排第几」,下标就能用了。 |
为什么可以离散化 | 名次的大小关系与原值完全一致,所以逆序对的个数一个都不变。这是它唯一能用的理由,换成「求和」这种依赖具体数值的操作就不行了。 |
unique 的语义 | 把相邻重复元素挪到末尾,返回新末尾的迭代器。它不删除元素,所以必须配 erase。 |
lower_bound | 返回第一个 ≥ x 的位置。减去 begin() 得 0 起下标,+1 才是树状数组要的 1 起下标。 |
为什么从 1 起 | i & -i 取最低位的 1。i = 0 时 i & -i = 0,add 会原地死循环——这就是名次必须 +1 的实际原因,不是「习惯」。 |
从右往左扫 | 统计「右边比它小的」:query(rk − 1) 就是答案的那一份贡献,扫完加进 bit 即可。 |
int n = 6, a[7] = {0, 5, 4, 2, 6, 3, 1}; // 下标 1 起
int bit[7] = {0};
vector<int> v(a + 1, a + n + 1); // 复制一份
sort(v.begin(), v.end()); // 1 2 3 4 5 6
v.erase(unique(v.begin(), v.end()), v.end());
long long ans = 0;
for (int i = n; i >= 1; --i) { // 从右往左
int rk = lower_bound(v.begin(), v.end(), a[i]) - v.begin() + 1;
ans += query(rk - 1); // 右边比它小的个数
add(rk);
}
// ans = 11bit 数组开 n + 1:名次最大是 n,写成 bit[n] 会在 rk = n 时越界写。
逆序对最多 n(n−1)/2 = 1.25×1011,答案必须 long long。
| 组成 | 含义 / 格式要求 |
|---|---|
nxt[i] 的含义 | t[1..i] 的最长 border 长度:既是前缀又是后缀、且不等于本身的最长子串。 |
j 的含义 | 处理 i 时 j = nxt[i−1],是「上一段已经配好的长度」。所以只需要比较 t[i] 与 t[j+1]。 |
失配回跳 | j = nxt[j]:放弃一部分已配好的前缀,退到「还可能是答案」的最长位置。这是全部技巧所在。 |
为什么均摊 O(m) | 每次成功匹配 j 加 1,每次回跳 j 至少减 1;j 总共只加 m 次,所以总共只减 m 次。 |
1 起下标 | 字符串从 t + 1 开始存,nxt[1] 恒为 0(全局数组默认值正好对)。 |
匹配时同一套循环 | 文本串上把 t 换成 s;j == m 时输出 i − m + 1,再 j = nxt[j] 继续找(允许重叠)。 |
char t[] = " ABA"; // t[1]='A' t[2]='B' t[3]='A'
int nxt[4] = {0, 0, 0, 0};
for (int i = 2, j = 0; i <= 3; ++i) {
while (j && t[i] != t[j+1]) j = nxt[j];
if (t[i] == t[j+1]) ++j;
nxt[i] = j;
}
// "ABA" -> nxt[1]=0 nxt[2]=0 nxt[3]=1nxt 存的是「border 长度」,不是「下一个该比的下标」。用的时候写成 t[nxt[j] + 1],差 1 就全错。
把 nxt 初始化成 −1(有些人喜欢这么写)会让 while 条件变成两层判断,本套统一用 0。
先看一道正解只有 6 行的题。它不难,但它把「综合题」的本质演示得很干净:把操作翻译成柱状图,答案就变成相邻两项的差——想清楚结构,代码自然只有一行。
有一条长为 n 的路,第 i 段下陷了 di。每天可以选一段连续的路段 [l, r],把这段路上每一段的深度都减 1(减到 0 为止,不能减成负数)。求把所有下陷填平(全部变成 0)最少需要多少天。
输入格式:第一行一个整数 n(1 ≤ n ≤ 105);第二行 n 个整数 d1…dn(0 ≤ di ≤ 104)。
输出格式:一行一个整数,表示最少天数。
① 把操作画出来。把 d 画成柱状图:第 i 根柱子的高度是 di。一次操作就是在某几根连续的柱子上横着切掉一层——被切的那几层在位置上必须连续、高度都 ≥ 1。
② 按高度分层。第 k 层 = 满足 di ≥ k 的所有位置。这一层被「深度不到 k」的位置切成了若干段,每段连续、且必须单独用一天切掉——因为一次操作只能覆盖连续区间,中间有空洞就跨不过去。
③ 数段数。第 k 层在位置 i 处新开一段,当且仅当 di ≥ k 而 di−1 < k(约定 d0 = 0)。固定 i,满足 di−1 < k ≤ di 的 k 恰好有 max(0, di − di−1) 个。
④ 答案。天数 = 所有层所有段的段数之和 = Σi=1..n max(0, di − di−1)。上界:按层从低到高切,第 k 层有几段就切几天,正好填平。下界:对固定的 i,考察「覆盖了 i 却没有覆盖 i−1」的操作——位置 i 一共要被覆盖 di 次,位置 i−1 一共 di−1 次,被同一次操作同时覆盖的次数不超过 di−1,所以这类操作至少 max(0, di − di−1) 次。对所有 i 求和,就是总天数的下界。
⑤ 样例走一遍。d0 = 0,序列 4 3 2 5 3 5:4−0 = 4、3−4 < 0 记 0、2−3 < 0 记 0、5−2 = 3、3−5 < 0 记 0、5−3 = 2,合计 4+3+2 = 9。
⑥ 复杂度。一次线性扫描 O(n),每段的贡献只与它的前一段有关,所以一个数组都不用开,空间 O(1)。答案上界是 Σdi ≤ 105 × 104 = 109,卡在 int 上限的一半,但累加一律写 long long。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
scanf("%d", &n);
long long ans = 0; // 上界 1e9,int 勉强够;这类累加统一 long long
int pre = 0; // 约定 d[0] = 0:第一段之前没有路
for (int i = 1; i <= n; ++i) {
int d;
scanf("%d", &d);
if (d > pre) ans += d - pre; // 只有「比前一段深」的部分要新开一层
pre = d; // 留给下一段比较
}
printf("%lld\n", ans);
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
long long ans = 0; | 答案最多 109,int 还剩一倍余量;但「求和」类变量统一 long long,这样题目把 di 改成 109 时不用回头改类型。 |
int pre = 0; | 给 a0 补一个 0。差分的第一项是 d1 − d0 = d1,所以第一段路该要的天数一分都不少。写成 pre = d(拿自己初始化)第一段就漏了。 |
if (d > pre) ans += d - pre; | 整题唯一的核心。d ≤ pre 时这一段的高度已经被左边「顺带切掉」了,不需要新的天数;d > pre 时高出的每一层都要在 i 处新起一段。 |
ans += d - pre | 加的是差值,不是 d。写成 ans += d 会按「每段各自填平」算,样例输出变成 22。 |
pre = d; | 更新基准。写成 pre = max(pre, d) 就错了:后面要和「紧邻的前一段」比,不是和前缀最大值比;反例 5 1 5,正确答案 5 + 0 + 4 = 9,用 max 会变成 5 + 0 + 0 = 5。 |
for (int i = 1; i <= n; ++i) | 从 1 数到 n,正好对应 d1..n;pre 里存的是 di−1,循环开始时是 d0 = 0。 |
边读边算 | 每段的贡献只跟它的前一段有关,所以不需要把整个数组存下来。数据范围 105 时开数组也没事,但能 O(1) 空间就不要 O(n)。 |
printf("%lld\n", ans) | long long 必须用 %lld。写成 %d 时高 32 位被丢掉,输出一个随机数。 |
0~10 分钟:只读题,不写代码。给每题写三行:数据范围、要用的模板、最容易写错的一处。按「能立刻写 → 有思路 → 没思路」排序,不要按题号顺序做。
10~30:A(二分 + Dijkstra)。先写对 Dijkstra,再把二分套上去。反过来写会同时有两个未验证的部件。
30~50:B(逆序对)。离散化错一处全盘皆错;先用 n = 5、值很小的数据手算验证,再交。
50~70:D(区间筛)。重点只有两个:起点取 max(p2, ⌈L/p⌉p)、i × p 用 long long。
70~90:C(LCS)。想清「为什么排列可以映射成 LIS」再动手,这一步想不通就别写。
90~115:E(KMP)。写不出 nxt 就先在纸上把「ABA」的 nxt 算一遍,照着递推抄。
115~120:只检查,不改算法。按下面的清单过一遍,每项打勾。赛场上最后 5 分钟改崩的,比改对的多。
WA,答案刚好差一格:二分的 check 边界没对齐。A 题里 check 的语义是 dist[n] ≤ b,写成 < 时 dist[n] == b 会被误判不可行;写成 ≤ b + 1 又会在超一点时误判可行。
RE / 数组越界:B 题离散化后名次从 1 开始,bit 必须开 n + 1。开成 n 时 rk = n 会写越界,症状是小数据正常、大数据第 2 个点就 RE。
WA,输出负数:B 题答案到 1.25×1011,用 int 累加直接溢出成负数。C 题的 LIS 长度 ≤ n,用 int 没问题——所以类型要按题算,不能一刀切。
TLE:A 题用 O(n2) 的朴素 Dijkstra。n = 104 时单次 108,再乘 14 次二分,必死。二分答案的总代价是「check 的代价 × log」,check 里任何 O(n2) 都会被放大 14 倍。
RE / 死循环:D 题的 i × i、p × p 用 int 算。463412 = 2147488281,比 INT_MAX(2147483647)还大 4634,溢出成负数,循环下标变负 → RE 或卡死。
WA,答案多 1:D 题忘了 1 不是素数。L = 1 的测试点会多算一个,而样例不含 L = 1,本地看不出来。
MLE:D 题按 R 的大小开标记数组。R 到 231 时开不出来;只能开 R − L + 1。
WA,KMP 全乱:E 题的 nxt 当成了「下一个比较位置」。nxt 存的是 border 长度,下一步永远比 t[nxt[j] + 1],那个 +1 丢了就全错。
格式错:E 题先输出每个出现位置(各占一行),最后一行才是 nxt 数组;写成一行里混着输出必 WA。
关掉页面,把例题 P5019 从空文件敲一遍并提交。
改一改①:把 if (d > pre) ans += d - pre; 改成统计下降量 if (d < pre) ans += pre - d;,样例跑出 4(1+1+0+2+0),而正确答案是 9。亲手确认「变深」才是新开层。
改一改②:把 long long 改回 int,构造 n = 105、每段都是 104 的数据,看 ans 会不会变成负数。这一步把「类型按数据范围定」变成肌肉记忆。
改一改③:先把差分存进数组 d[i] = a[i] - a[i-1],再对 d 求前缀和还原 a,验证「差分 + 前缀和 = 原数组」,顺便确认 Σd = an。
改一改④:输入 5 1 5,正确答案是 5 + 0 + 4 = 9。把 pre 改成 pre = max(pre, d) 再跑,结果变成 5——这就是「和前缀最大值比」与「和紧邻前一项比」的区别。
| 题面里的词 | 第一反应 | 本套哪题 |
|---|---|---|
| 「最大值最小」「最小的最大值」 | 二分答案 + 单调性 check | A(P1462) |
| 「有多少对 i < j 且 ai > aj」 | 逆序对:离散化 + 树状数组 / 归并 | B(P1908) |
| 「两个排列的最长公共子序列」 | 映射成位置序列,再求 LIS | C(P1439) |
| 「区间内素数个数」「R 到 231」 | 筛到 √R,再在区间上打标记 | D(P1835) |
| 「一个串在另一个串里的出现位置」 | KMP:先 nxt 再匹配 | E(P3375) |
| 「比前一段多出的部分」「新开几段」 | 差分:Σ max(0, ai − ai−1) | 例题(P5019) |
| 「左边严格递增、右边严格递减,中间一个峰」 | 正着一遍、反着一遍,再枚举峰 | 第 31 节的合唱队形(P1091) |
数组大小 ≥ 数据范围上限 + 5;n、m 各开一个,别共用一个 MAXN 常量就以为够了。
long long:逆序对、最短路、所有求和;输入输出用 %lld。
离散化后名次从 1 开始了吗?树状数组的下标出现 0 了吗?
二分退出后,check 的可行性是否已验证(不可行要先输出无解标记)?
边界:n = 1、L = 1、全部相等、答案是 0,这四种输入跑过吗?
输出格式:几个数、一行还是一列、行末有没有多余空格。
样例逐字对过吗?不是「看起来一样」,是每一位都一样。
限时 100 分钟。每题先自己写 20~25 分钟;写不出来先看提示,再给自己 10 分钟;还不行才点开思路。每题都独立限时,不要一题耗死。
有 n 个城市(编号 1…n)和 m 条双向道路。穿过第 i 条道路会失去 wi 点生命值;进入第 i 个城市要交 fi 点费用,起点和终点也要交。你从 1 号城市出发到 n 号城市,初始生命值 b。
一条路径的「花费」定义为:路径上所有城市费用的最大值。要求在沿途失去生命值总和不超过 b 的前提下,让这个最大值尽量小,输出最小值;若不存在这样的路径,输出 AFK(三个大写字母)。
输入格式:第一行三个整数 n、m、b(1 ≤ n ≤ 104,1 ≤ m ≤ 5×104,1 ≤ b ≤ 109);第二行 n 个整数 f1…fn(1 ≤ fi ≤ 109);接下来 m 行,每行三个整数 u、v、w(1 ≤ w ≤ 109),表示 u、v 之间有一条双向道路。
输出格式:一行一个整数,或 AFK。
样例输入4 4 10
5 4 3 2
1 2 3
2 4 4
1 3 2
3 4 10
样例输出5
样例解释:1→2→4 失去 3+4 = 7 点生命值,经过的城市费用最大是 max(5,4,2) = 5;1→3→4 虽然费用更小,但要失去 2+10 = 12 点,超过 b = 10。所以答案是 5。
这题有两个代价:生命值总和、路径上最贵的城市。它们不是同一个量,不能合成一次最短路。
先规定「只允许走费用 ≤ X 的城市」,剩下的就是一个纯粹的最短路问题。X 变大时能走的城市只多不少,最短距离只会变小——这个单调性说明了什么?
答案一定是某个城市的费用,候选只有 n 个。逐个试要跑 n 次最短路,但候选是有序的,能不能只试十几次?
把问题拆成两层:外层是对「路径上城市费用最大值」的二分,内层是普通的单源最短路。
判定问题:给定 lim,只保留费用 ≤ lim 的城市,跑 1 到 n 的最短路,若 dist[n] ≤ b 则 lim 可行。
单调性:lim 增大时可用城市集合只增不减,最短路只可能变短,所以可行 lim 构成一个后缀——可以二分。
候选集是所有城市费用排序去重后的数组。先判最大候选是否可行,不可行直接输出 AFK;否则二分出最小的可行下标。
细节:起点 1 和终点 n 也要交费,所以 f[1] > lim 或 f[n] > lim 时直接不可行。漏了这一句,当 lim 落在 f[1] 与 f[n] 之间时会输出一个实际上走不通的答案。
复杂度:离散化 O(n log n);二分 log n ≈ 14 次,每次 Dijkstra O(m log n) ≈ 5×104 × 14,总计约 107 次操作。最长简单路径不过 104 条边,每条 109,最短路最大 1013,dist 必须 long long。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10005;
typedef long long ll;
typedef pair<ll, int> pli;
int n, m;
ll b;
ll f[MAXN]; // 每个城市的费用
vector<pair<int,int>> g[MAXN]; // (终点, 生命值消耗)
ll dist[MAXN];
bool check(ll lim) { // 只允许走费用 <= lim 的城市
if (f[1] > lim || f[n] > lim) return false; // 起终点也要交费
const ll INF = (ll)4e18;
fill(dist, dist + n + 1, INF);
dist[1] = 0;
priority_queue<pli, vector<pli>, greater<pli>> pq; // 小根堆:(距离, 编号)
pq.push({0, 1});
while (!pq.empty()) {
pli cur = pq.top(); pq.pop();
ll d = cur.first;
int u = cur.second;
if (d > dist[u]) continue; // 过期版本
if (d > b) continue; // 已经超过血量,再走只会更贵
for (auto &e : g[u]) {
int v = e.first, w = e.second;
if (f[v] > lim) continue; // 这个城市太贵,不能进
if (dist[v] > d + w) {
dist[v] = d + w;
pq.push({dist[v], v});
}
}
}
return dist[n] <= b;
}
int main() {
scanf("%d%d%lld", &n, &m, &b);
for (int i = 1; i <= n; ++i) scanf("%lld", &f[i]);
for (int i = 0; i < m; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
g[u].push_back({v, w});
g[v].push_back({u, w}); // 双向道路,两条边都要存
}
vector<ll> val(f + 1, f + n + 1); // 候选集:所有城市费用
sort(val.begin(), val.end());
val.erase(unique(val.begin(), val.end()), val.end());
if (!check(val.back())) { // 最宽松的上界也不行 → 无解
puts("AFK");
return 0;
}
int lo = 0, hi = (int)val.size() - 1;
while (lo < hi) {
int mid = (lo + hi) / 2;
if (check(val[mid])) hi = mid; // 可行:答案不右于 mid
else lo = mid + 1;
}
printf("%lld\n", val[lo]);
return 0;
}| 片段 | 作用 |
|---|---|
if (f[1] > lim || f[n] > lim) return false; | 起终点同样收费。少了这一句,lim 落在 f[1] 与 f[n] 之间时会把一条根本进不去的路径判成可行。 |
const ll INF = (ll)4e18; | 最短路最大 1013,INF 取 4×1018 既不溢出 long long,又远大于任何真实距离。 |
if (d > dist[u]) continue; | 懒删除:堆里同一个点可能有好几份历史版本,取出时距离对不上就丢掉。 |
if (d > b) continue; | 血量已经超了,继续扩展只会更长。剪掉它同时也保证了 d + w 不会溢出。 |
if (f[v] > lim) continue; | ★ 二分的关键:不行就整条边跳过。这一步删掉,check 就与 lim 无关了。 |
if (!check(val.back())) puts("AFK"); | 先判最宽松的上界。不判的话,不可行时二分仍会收敛到最后一个下标,程序会输出一个可行的假答案。 |
if (check(val[mid])) hi = mid; | 可行就往左收(mid 本身可能是答案,所以不是 mid − 1)。 |
lo = mid + 1; | 不可行就整段右移。写成 mid 会在不可行时原地打转,死循环。 |
g[v].push_back({u, w}); | 双向道路必须存两条。只存一条时大量点不可达,样例就会输出 AFK。 |
给定一个长度为 n 的序列 a,求逆序对的个数。逆序对指满足 i < j 且 ai > aj 的数对 (i, j)。
输入格式:第一行一个整数 n(1 ≤ n ≤ 5×105);第二行 n 个整数 a1…an(1 ≤ ai ≤ 109)。
输出格式:一行一个整数,表示逆序对个数。
样例输入6
5 4 2 6 3 1
样例输出11
样例解释:以每个位置为右端点,左边比它大的个数依次是 0、1、2、0、3、5,合计 11。
暴力是 O(n2):n = 5×105 时有 1.25×1011 对,1 秒绝无可能。要把「数一遍右边比它小的」这一步压到 log。
ai 到 109,当不了数组下标。但逆序对只关心大小关系,不关心具体数值——能不能只保留「排第几」?
从右往左扫,每读到一个数就查一次「已经加入的里面比它小的有几个」。什么东西能同时支持「单点加 1」和「前缀求和」,而且都是 log?
逆序对 = Σi(i 右边比 ai 小的个数)。把统计对象从「数对」换成「每个位置的一份贡献」。
从右往左扫,维护一个集合 = 已经扫过的数(也就是 i 右边的数)。查询「比 ai 小的有几个」,就是一次前缀求和;把 ai 加入集合,就是一次单点加 1。树状数组两个操作都是 O(log n)。
下标问题:ai 到 109,树状数组装不下。做离散化——把所有值排序去重,把 ai 换成它的名次 rk(1…n)。名次之间的大小关系与原值完全一致,所以逆序对个数不变。
于是 rk − 1 就是「严格小于」的范围:query(rk − 1) 天然排除了相等的情况,题目要的正是严格大于。
复杂度 O(n log n):n = 5×105 时约 5×105 × 19 × 2 ≈ 2×107 次操作。
答案上界 n(n−1)/2 = 1.25×1011,必须 long long。另一条路是归并排序:合并两个有序段时,右段每个元素被取走前,左段剩下的元素都与它构成逆序对(第 21、56 节的递归分治)。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 500005;
int n, a[MAXN], bit[MAXN]; // bit 存的是「名次 <= i 的已出现个数」
void add(int i) { // 名次 i 上出现了一个数
for (; i <= n; i += i & -i) ++bit[i];
}
int query(int i) { // 名次 <= i 的已出现个数,也就是前缀和
int s = 0;
for (; i > 0; i -= i & -i) s += bit[i];
return s;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
vector<int> v(a + 1, a + n + 1); // 复制一份用来离散化
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());
long long ans = 0; // 最多 1.25e11,必须 long long
for (int i = n; i >= 1; --i) { // 从右往左:集合里全是「右边」的数
int rk = lower_bound(v.begin(), v.end(), a[i]) - v.begin() + 1;
ans += query(rk - 1); // 右边严格小于它的个数
add(rk);
}
printf("%lld\n", ans);
return 0;
}| 片段 | 作用 |
|---|---|
vector<int> v(a + 1, a + n + 1); | 把有效数据复制出来。a 是 1 起下标,所以区间是 [a+1, a+n+1)。 |
sort + unique + erase | 升序去重。unique 只是把重复元素挪到末尾,必须接 erase 才是真去重。 |
... - v.begin() + 1 | 名次从 1 开始。树状数组的 i & -i 在 i = 0 时等于 0,add(0) 会原地死循环——这个 +1 不是习惯,是必需。 |
for (int i = n; i >= 1; --i) | 从右往左扫,保证 bit 里存的全是 i 右边的数。从左往右扫再查 query(rk−1),数出来的是「顺序对」。 |
ans += query(rk - 1); | rk − 1 是「严格小于它」的名次范围,相等的情况被排除,正好对应 ai > aj。 |
add(rk); | 统计完再插入自己。顺序反了会把 ai 自己算进去,答案偏大。 |
long long ans | 最大 5×105 × 5×105 / 2 ≈ 1.25×1011,int 最多 2.1×109。 |
i += i & -i / i -= i & -i | 上跳与下跳。i = 0 时 i & -i 也是 0,add(0) 会原地打转,所以名次必须是 [1, n]。 |
给出 1…n 的两个排列 P1 和 P2,求它们的最长公共子序列的长度。
输入格式:第一行一个整数 n(1 ≤ n ≤ 105);第二行 n 个整数,是 P1;第三行 n 个整数,是 P2。
输出格式:一行一个整数,表示最长公共子序列的长度。
样例输入5
3 2 1 4 5
1 2 3 4 5
样例输出3
样例解释:2 4 5、3 4 5、1 4 5 都是公共子序列,最长长度为 3。
普通 LCS 的 DP 是 O(n2):1010 个状态,1 秒绝无可能。必须用上「两个都是排列」这个条件。
排列里每个数只出现一次,所以「这个数在 P1 里排第几位」是唯一确定的。把 P2 的每个数换成它的位置,会得到一串什么?
一个公共子序列在 P1 里的位置是递增的。那么这串位置里,公共子序列长什么样?
先建映射:pos[x] = x 在 P1 中出现的位置(1…n,每个 x 恰好一个)。
把 P2 逐项换成 pos 值,得到序列 c:ci = pos[P2,i]。
关键等价:P1 与 P2 的公共子序列,与 c 的上升子序列一一对应。因为一个子序列同时是 P1 的子序列,意味着它的元素在 P1 中的位置按顺序递增——而 c 的下标顺序就是它在 P2 中的顺序,c 的值就是它在 P1 中的位置。两个顺序都对上,长度自然相等。
于是问题变成「求 c 的最长上升子序列」,用 tail 数组 + 二分做 O(n log n):tail[len] 记录「长度为 len 的上升子序列的最小结尾」,每个 ci 用 lower_bound 找到第一个 ≥ 它的位置替换掉。
为什么可以替换:结尾越小,后面能接的数越多,所以「最小结尾」这一状态足以代表全部长度为 len 的子序列。
c 是 1…n 的一个排列,元素互不相同,所以 lower_bound 与 upper_bound 结果一致;若换成有重复的序列求「不下降子序列」,就得用 upper_bound。复杂度 O(n log n)。
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int pos[N], c[N], tail[N]; // tail[len]:长度为 len 的上升子序列的最小结尾
int main() {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; ++i) { // 第一个排列:记下每个数在第几位
int x;
scanf("%d", &x);
pos[x] = i;
}
for (int i = 1; i <= n; ++i) { // 第二个排列:换成「在第一个排列里的位置」
int y;
scanf("%d", &y);
c[i] = pos[y];
}
int len = 0; // 当前 LIS 长度
for (int i = 1; i <= n; ++i) {
// 找到第一个 tail >= c[i] 的位置;tail[1..len] 是单调不减的
int p = (int)(lower_bound(tail + 1, tail + len + 1, c[i]) - tail);
tail[p] = c[i]; // 用更小的结尾替换,给后面留更多余地
if (p == len + 1) ++len; // 接到了末尾,长度加一
}
printf("%d\n", len);
return 0;
}| 片段 | 作用 |
|---|---|
pos[x] = i; | 读第一个排列时就建好映射。之后每查一次「x 在 P1 的位置」都是 O(1)。 |
c[i] = pos[y]; | 全题唯一的转化,写在两行里。转化完之后原题的两个排列都不再需要。 |
lower_bound(tail + 1, tail + len + 1, c[i]) | 在 [1, len] 里找第一个 ≥ c[i] 的位置。区间右端是 tail + len + 1,别写成 tail + len。 |
tail[p] = c[i]; | 替换而不是插入。同一个长度只保留最小结尾,状态数才是 O(n)。 |
if (p == len + 1) ++len; | p 越过 len 说明 c[i] 比所有结尾都大,可以接在最长的后面,长度加一。 |
int len = 0; | 空序列的 LIS 长度是 0。tail 是全局数组,默认全 0,但只有 1…len 会被读到。 |
printf("%d\n", len); | 答案是长度,不是子序列本身。题目没要求输出方案。 |
给定区间 [L, R],求这个区间内素数的个数。
输入格式:一行两个正整数 L、R(1 ≤ L ≤ R ≤ 231 − 1,且 R − L ≤ 106)。
输出格式:一行一个整数,表示区间内素数的个数。
样例输入1 10
样例输出4
样例解释:1 到 10 之间的素数是 2、3、5、7,共 4 个;1 不是素数。
区间长度最多 106,整段打标记完全放得下;但 L、R 本身到 231,按 R 开数组是不可能的。注意区分「区间长度」和「数值大小」。
要判断 [L, R] 里每个数是不是素数,只需要知道它有没有不超过 √R 的因子。√(231) ≈ 46341——这个范围内的素数筛得出来。
用每个小素数 p 去划掉 [L, R] 里 p 的倍数。起点从哪儿开始?如果从 L 开始,当 L ≤ p 时会把 p 自己也划掉。
两步走:先筛出 [2, √R] 的全部素数(埃氏筛,√R ≤ 46341,数组几万个就够了);再用这些小素数去「筛掉」[L, R] 里的合数。
第二步:对每个小素数 p,把 [L, R] 中 p 的倍数标记为合数。起点取 max(p2, ⌈L/p⌉·p):从 p 开始是为了不划掉 p 本身,从 ⌈L/p⌉·p 开始是为了不划掉区间左边的数。
标记数组只需要 R − L + 1 个位置,用 x − L 当下标。这就是「区间筛」的全部内容。
为什么只筛到 √R:任何合数 x ≤ R 都有一个不超过 √x ≤ √R 的因子;这个因子对应的素数一定在筛出的表里。
边界:L = 1 时要把 1 单独标成非素数;L = R 时答案要么 1 要么 0。
溢出:p ≤ 46341,p × p 最大 463412 = 2147488281,比 INT_MAX(2147483647)还大 4634,用 int 算会变成负数,循环下标变负 → RE。所有涉及乘积的地方一律 long long。
复杂度:筛小素数 O(√R log log R),标记区间 Σ (R−L)/p = O((R−L) log log R),R = 231 时约 4.6×104 + 106 × 3 次操作。
#include <bits/stdc++.h>
using namespace std;
const int MAXS = 1000005;
const int MAXP = 50005; // sqrt(2^31 - 1) ≈ 46341
bool isp[MAXP]; // 小素数表
bool mark[MAXS]; // mark[i] 表示 L + i 是合数
int main() {
long long L, R;
scanf("%lld%lld", &L, &R);
int lim = (int)sqrt((double)R) + 1; // 只需要筛到 sqrt(R)
fill(isp, isp + lim + 1, true);
isp[0] = isp[1] = false;
for (int i = 2; i <= lim; ++i)
if (isp[i])
for (long long j = 1LL * i * i; j <= lim; j += i) // i*i 用 long long 算
isp[j] = false;
long long len = R - L + 1;
if (L == 1) mark[0] = true; // 1 不是素数
for (int i = 2; i <= lim; ++i) {
if (!isp[i]) continue;
long long p = i;
// 从 p 的平方 或 第一个 >= L 的倍数 开始划;两者取大,避免划掉 p 自己
long long st = max(p * p, (L + p - 1) / p * p);
for (long long x = st; x <= R; x += p)
mark[x - L] = true;
}
int ans = 0;
for (long long i = 0; i < len; ++i)
if (!mark[i]) ++ans;
printf("%d\n", ans);
return 0;
}| 片段 | 作用 |
|---|---|
const int MAXP = 50005; | 46342 个位置就够,开 50005 留余量。写 46341 会差一点点就越界。 |
fill(isp, isp + lim + 1, true); | 初始化 [0, lim] 全部为 true,右端写 lim + 1。区间少一个,isp[lim] 就是 false,最后一段素数会被漏筛。 |
1LL * i * i | ★ i 到 46341 时 i*i 超过 INT_MAX,用 int 会溢出成负数,内层循环直接跑飞。 |
if (L == 1) mark[0] = true; | 1 不是素数。样例就是 L = 1,漏了这一行输出 5。 |
long long p = i; | 后面的 p*p 与 x 都要 long long,先在这里提上来,免得写成 (long long)i*i 到处都是。 |
max(p * p, (L + p - 1) / p * p) | 两个起点取大:p*p 保证不划掉 p 自己,⌈L/p⌉p 保证落在区间内。漏掉 p*p,L ≤ p 时答案会少 1。 |
mark[x - L] = true; | 下标平移。写成 mark[x] 需要 231 个位置,直接 MLE 崩溃。 |
for (long long x = st; x <= R; x += p) | x 用 long long,否则 x += p 可能越过 231 变成负数,死循环。 |
if (!mark[i]) ++ans; | 未被标记的就是素数。ans ≤ R − L + 1 ≤ 106 + 1,int 足够。 |
给出两个字符串 s1 和 s2。若 s1 的区间 [l, r] 子串与 s2 完全相同,则称 s2 在 s1 中出现,出现位置为 l。要求输出 s2 在 s1 中所有出现的位置。
另外定义:字符串 s 的 border 是 s 的一个不等于 s 本身的子串 t,满足 t 既是 s 的前缀又是 s 的后缀。对 s2 的每个前缀(长度为 1…|s2|),求出它最长 border 的长度。
输入格式:第一行一个字符串 s1,第二行一个字符串 s2(1 ≤ |s2| ≤ |s1| ≤ 106,只含大小写字母)。
输出格式:若干行,每行一个整数,按从小到大输出 s2 在 s1 中的出现位置;最后一行输出 |s2| 个整数,第 i 个表示 s2 长度为 i 的前缀的最长 border 长度,相邻两个用一个空格隔开。
样例输入ABABABC
ABA
样例输出1
3
0 0 1
样例解释:ABA 在 ABABABC 的第 1 位和第 3 位出现(允许重叠);前缀 A、AB 没有非自身的 border,ABA 的 border 是 A,长度 1。
暴力是每个起点比 |s2| 个字符,最坏 106 × 106。要压到线性,必须让「已经匹配上的那一段」在失配后不被浪费。
失配时,已经匹配的那一段文本,既是 s2 的某个前缀,又是刚读过的文本的后缀。如果知道 s2 每个前缀「最长的、既是前缀又是后缀的长度」,失配后应该退到哪?
这个长度可以递推:nxt[i] 只依赖 nxt[i−1] 和 t[i] 本身。为什么回跳的总次数是 O(m) 而不是 O(m2)?
定义 nxt[i] = s2[1..i] 的最长 border 长度(即最长的、既是前缀又是后缀、且不等于本身的长度)。
递推:假设 nxt[i−1] = j 已经算出。若 s2[i] == s2[j+1],则 nxt[i] = j + 1;否则令 j = nxt[j] 继续试,直到 j = 0 或字符相等。这一步的依据是:i 位置的所有可行 border 长度,必然依次是 nxt[i−1]、nxt[nxt[i−1]]、…(border 链)。
匹配:在 s1 上维护 j = 「当前已匹配的长度」。s1[i] 与 s2[j+1] 相等则 ++j,否则 j = nxt[j] 回跳。j 达到 m 时输出起点 i − m + 1,然后令 j = nxt[j] 继续(允许重叠)。
复杂度:j 每次成功匹配加 1,总共只加 O(n) 次;每次回跳 j 至少减 1,所以回跳总次数也是 O(n)。均摊下来整个匹配 O(n + m),106 的规模毫无压力。
为什么不能 j = 0 直接重来:那样已经匹配的那一段信息全丢了,退化成暴力。
输出注意:位置从 1 开始数,所以起点是 i − m + 1;最后一行才是 nxt 数组,nxt[1] = 0 也要输出。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
char s[MAXN], t[MAXN];
int nxt[MAXN];
int main() {
scanf("%s", s + 1); // 从下标 1 开始存:位置直接是 i - m + 1
scanf("%s", t + 1);
int n = (int)strlen(s + 1), m = (int)strlen(t + 1);
// ① 求 t 的前缀函数:nxt[i] = t[1..i] 的最长 border 长度
for (int i = 2, j = 0; i <= m; ++i) {
while (j && t[i] != t[j + 1]) j = nxt[j]; // 失配:沿 border 链回退
if (t[i] == t[j + 1]) ++j; // 接上一位,匹配长度加一
nxt[i] = j;
}
// ② 在 s 上匹配:j 是「当前已经匹配的长度」
for (int i = 1, j = 0; i <= n; ++i) {
while (j && s[i] != t[j + 1]) j = nxt[j];
if (s[i] == t[j + 1]) ++j;
if (j == m) { // 完整出现一次
printf("%d\n", i - m + 1); // 起点(1 起)
j = nxt[j]; // 允许重叠,继续找下一处
}
}
// ③ 最后一行:nxt[1..m]
for (int i = 1; i <= m; ++i) {
if (i > 1) putchar(' ');
printf("%d", nxt[i]);
}
putchar('\n');
return 0;
}| 片段 | 作用 |
|---|---|
scanf("%s", s + 1); | 从 1 起存。这样「起点」与「已匹配长度」的换算就是 i − m + 1,不用再 +1 修正。 |
for (int i = 2, j = 0; i <= m; ++i) | 从 2 开始:nxt[1] 恒为 0(单个字符没有非自身的 border)。j 的初值 0 表示「还没有任何 border」。 |
while (j && t[i] != t[j + 1]) j = nxt[j]; | ★ 全题的核心。失配时放弃一部分前缀,退到下一个候选 border。while 而不是 if:一次退不够,可能连退好几次。 |
t[j + 1] | 要比较的位置是「当前匹配长度 + 1」。写成 t[j] 就整体错位一位,样例立刻错。 |
nxt[i] = j; | 把当前匹配长度记下来,它既是 border 长度,也是下一个 i 的回退起点。 |
if (j == m) { ... } | 匹配成功。先输出起点,再 j = nxt[j] 继续——写成 j = 0 会漏掉重叠的出现(样例的第 3 位就没了)。 |
i - m + 1 | 起点。1 起下标下,终点是 i、长度是 m,起点自然是 i − m + 1。 |
if (i > 1) putchar(' '); | nxt 数组用空格分隔、行末只换行。多一个空格在严格评测里可能 WA。 |
char s[MAXN], t[MAXN]; | 106 的字符串用 char 数组 + scanf 最稳;string 也能过,但常数大一些。 |