这一节不讲新知识。5 道题、150 分钟,考第 1~49 节里复用率最高的五件事:单调队列(第 48 节)、正反两遍 LIS(第 31 节)、二分答案加贪心判定(第 27、16 节)、树状数组加离散化(第 46、13 节)、断环成链的区间 DP(第 33、18 节)。五道题都是洛谷真题,代码只用你已经学过的东西写。满分 500 分,每题 100 分。
先看清单。卡住时不要顺着题号往下问,直接翻回对应的节重看那一段。
| 题号 | 题目 | 对应节 | 一句话考点 | 建议用时 |
|---|---|---|---|---|
| A | P1886 滑动窗口 | 第 28、48 节 | 队尾弹逆序、队头出窗口 | 22 分钟 |
| B | P1091 合唱队形 | 第 31 节 | 正反两遍 LIS 再合并 | 23 分钟 |
| C | P2678 跳石头 | 第 27、16 节 | 二分最短距离 + 贪心数移走几块 | 28 分钟 |
| D | P1908 逆序对 | 第 46、13 节 | 离散化后上树状数组 | 30 分钟 |
| E | P1880 石子合并 | 第 33、18 节 | 断环成链 + 区间 DP | 32 分钟 |
难度上 A~D 是普及/提高-,E 是提高。本场的分水岭在 C 和 E:C 卡在“check 怎么写”,E 卡在“环怎么处理”。这两处都是想通就五分钟、想不通就零分的地方,所以下面每道题的提示都从“先想暴力”开始,而不是从“先想正解”开始。
总原则:前 55 分钟只做 A、B,并且必须一次过。这两题都是“想到就能写对”的题,一旦返工,后面三道题的时间就是借来的。
| 时间段 | 做什么 | 关键动作 |
|---|---|---|
| 0 ~ 8 | 通读 5 题 | 在纸上给每题标:会做 / 有思路 / 完全不会。这 8 分钟不写一行代码。 |
| 8 ~ 30 | A 滑动窗口(22) | 先写最小值那一遍,测完样例,再复制一份把比较方向反一下。 |
| 30 ~ 53 | B 合唱队形(23) | 两遍 LIS 都不难,难在最后的 n − max(f+g−1) 别算成 max。 |
| 53 ~ 81 | C 跳石头(28) | 先写 check,再套二分。check 写完用小数据手算两遍再编译。 |
| 81 ~ 111 | D 逆序对(30) | 离散化那三行写完先输出 sz 验一遍,再写树状数组。 |
| 111 ~ 143 | E 石子合并(32) | 断环成链 + 长度递增。到点没 AC 就交线性版本保分。 |
| 143 ~ 150 | 检查 | 只查数组大小、long long、输出格式,不改算法。 |
每题内部再切一刀:想 5 分钟、写 15 分钟、测 5 分钟。想的那 5 分钟不许碰键盘,纸上必须出现 4 行东西:
键盘会骗你:能编译通过的代码看起来总是对的。而这 4 行一旦写错,编译器一声不响。
本场五道题的丢分点几乎全在这 4 行里:A 的窗口左端点、B 的 n−best、C 的上取整、D 的 long long、E 的 2n 上界。
熟练之后这 5 分钟会压缩到 60 秒,但绝不能省。
开赛前 10 分钟做一道不计分的热身题,把“读题 → 排序 → 双指针 → 输出”这条最常用的链路跑热。它顺手复习了本场 C 题 check 函数要用的贪心:能塞就塞,塞不下就另起。
元旦快到了,班长要把买来的纪念品按价格分组发给同学。每组最多只能放两件纪念品,并且每组纪念品的价格之和不能超过一个给定的上限 w。求最少需要分成几组。
输入格式:第一行一个整数 w(每组价格之和的上限);第二行一个整数 n(纪念品件数,n ≤ 30000);第三行 n 个正整数,表示每件纪念品的价格(均不超过 w)。
输出格式:一个整数,即最少的分组数。
每组最多两件,所以答案的形状只有两种:一件一组,或一大一小配成一组。配得越多,组数越少。
复杂度 O(n log n),瓶颈在排序;双指针部分只有 O(n) 次比较。n = 30000 时几乎瞬间出答案。
#include <bits/stdc++.h>
using namespace std;
int w, n, p[30005];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> w >> n;
for (int i = 1; i <= n; ++i) cin >> p[i];
sort(p + 1, p + n + 1); // 双指针的前提:有序
int l = 1, r = n, ans = 0;
while (l <= r) {
if (l == r) { ++ans; break; } // 只剩一件,单独一组
if (p[l] + p[r] <= w) { ++l; --r; } // 最便宜的配最贵的
else --r; // 配不上:最贵的自己一组
++ans;
}
cout << ans << "\n";
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
sort(p + 1, p + n + 1); | 双指针的正确性依赖有序:只有排好序才能说“最贵的配谁都超”。区间左闭右开,右端写 n+1。 |
int l = 1, r = n, ans = 0; | ans 必须初始化为 0。局部变量不赋值就是垃圾值,累加后答案随机。 |
while (l <= r) | 区间还没空就继续。不是 l < r:l == r 时还剩一件,要单独成组。 |
if (l == r) { ++ans; break; } | 先处理只剩一件的情况,否则后面 p[l] + p[r] 会把同一件算两遍。 |
if (p[l] + p[r] <= w) | 用 ≤ 不用 <:等于上限也是合法的。这里写错,边界数据全错。 |
++l; --r; | 配成一组后两个指针都动。只动一个的话,p[l] 会被重复使用。 |
else --r; | 最贵的配不上最小的,那它配谁都不行,只能自己一组。l 不动,留给下一轮配更便宜的大件。 |
cout << ans << "\n"; | 只输出一个整数加换行。题目没要求任何提示语。 |
热身完,看四张模板卡。它们不是新知识,是把本场五道题各自要用的骨架再默写一遍——赛场上默写不出来的模板,等于没学过。先能一次默写正确,再开始计时。
| 组成 | 含义 / 格式要求 |
|---|---|
q[++t] = i | 队列里存下标,不存值。存下标才能判断“是否滑出窗口”,也能反查 a[q[h]]。 |
while (h <= t && a[q[t]] >= a[i]) --t | 求最小值用 >=;求最大值把 >= 换成 <=,其余不动。方向写反,队头就不是最值。 |
h <= t | 空队列判据:h = t + 1 即空。写成 h < t 会把“只剩一个元素”误判成空队列。 |
if (q[h] < i - k + 1) ++h | 窗口左端点是 i − k + 1。每步最多过期一个下标,所以用 if 就够,不必 while。 |
if (i >= k) | 前 k − 1 步窗口还没填满,不能输出。 |
总复杂度 O(n) | 每个下标进队一次、出队一次,while 的总执行次数不超过 n。暴力是 O(nk),最坏 10¹²。 |
#include <bits/stdc++.h>
using namespace std;
int a[6] = {0, 5, 3, 8, 1, 9}; // 1 开始存,n = 5
int q[10];
int main() {
int n = 5, k = 3, h = 1, t = 0;
for (int i = 1; i <= n; ++i) {
while (h <= t && a[q[t]] >= a[i]) --t;
q[++t] = i;
if (q[h] < i - k + 1) ++h;
if (i >= k) cout << a[q[h]] << " "; // 输出 3 1 1
}
return 0;
}队列里存下标、不存值——这是这一节唯一需要背死的一句话。
求最大值时,队尾弹出条件和答案取值都要反过来;只改一半是这类题最常见的错法。
| 组成 | 含义 / 格式要求 |
|---|---|
lowbit(x) = x & -x | 取 x 最低位的 1 所代表的权值:lowbit(6) = 2,lowbit(8) = 8,lowbit(0) = 0。 |
add(i, v) | 把 i 这条链上的结点都加 v:i, i+lowbit(i), i+lowbit(i+lowbit(i)) …,直到超过 sz。O(log n)。 |
sum(i) | 前缀和:t[i] + t[i−lowbit(i)] + … 直到下标 0。O(log n)。区间和 = sum(r) − sum(l−1)。 |
下标必须 ≥ 1 | lowbit(0) = 0 会让 add 的循环变量原地不动,死循环 → TLE。 |
sz | 值的上界(本题是离散化后不同值的个数)。add 的终止条件是 i <= sz,不是 i <= n。 |
#include <bits/stdc++.h>
using namespace std;
int t[20], sz = 8;
int lowbit(int x) { return x & -x; }
void add(int i, int v) { for (; i <= sz; i += lowbit(i)) t[i] += v; }
int sum(int i) { int s = 0; for (; i > 0; i -= lowbit(i)) s += t[i]; return s; }
int main() {
add(3, 1); add(3, 1); add(6, 1); // 排名 3 出现两次,排名 6 出现一次
cout << sum(2) << "\n"; // 0:排名 ≤ 2 的没有
cout << sum(3) - sum(2) << "\n"; // 2:排名恰为 3 的个数
cout << sum(6) << "\n"; // 3:排名 ≤ 6 的总个数
return 0;
}sum 与 add 的循环方向相反:一个 i -= lowbit(i),一个 i += lowbit(i)。写反就是死循环或答案偏小。
求逆序对时查 sum(r−1) 而不是 sum(r),否则相等元素会被算成逆序对。
本题每次只“加 1”,所以 D 题代码里把 add 简化成 void add(int i) { ++t[i]; },省掉了加数参数。
| 组成 | 含义 / 格式要求 |
|---|---|
sort(b + 1, b + n + 1) | 先把原数组复制一份到 b,排序。a 本身不动,后面还要按下标扫。 |
unique(b+1, b+n+1) | 把相邻的重复元素搬到末尾,返回“去重后区间的尾后指针”。它不删除元素,只是搬家。 |
- (b + 1) | 去重后的元素个数 sz。写成 - b 会多 1,是所有离散化 bug 里最常见的一个。 |
lower_bound(b+1, b+sz+1, x) - b | 第一个 ≥ x 的位置,即 x 的排名(从 1 开始)。右端点是 sz+1,不是 n+1。 |
相等元素拿到同一个排名 | 这正是需要的:逆序对要求严格大于,用 r−1 查询天然排除相等的情况。 |
#include <bits/stdc++.h>
using namespace std;
int a[6] = {0, 1000000000, -5, 7, 7, 233};
int b[6];
int main() {
int n = 5;
for (int i = 1; i <= n; ++i) b[i] = a[i];
sort(b + 1, b + n + 1);
int sz = unique(b + 1, b + n + 1) - (b + 1); // 4 个不同值
for (int i = 1; i <= n; ++i)
cout << lower_bound(b + 1, b + sz + 1, a[i]) - b << " ";
// 输出 4 1 2 2 3
return 0;
}if (x < 1 || x > sz) 说明离散化写错了:出现这种情况几乎都是 sz 的表达式少减了一次。
离散化不改变答案的正确性,它只是把“值域”换成“排名”,让桶和树状数组开得下。
| 组成 | 含义 / 格式要求 |
|---|---|
a[i + n] = a[i] | 把数组复制一份接在后面。环上任意一种“从哪断开”的走法,都对应 2n 链上一个长度为 n 的连续区间。 |
for (len = 2; len <= n; ++len) | 按区间长度从小到大算。区间 DP 的方向是“先短后长”,长度 1 的区间是初值。 |
i + len - 1 <= 2 * n | 右端点不能越过 2n。只写 i <= n 是不够的:短区间的右端点完全可以超过 n。 |
for (k = i; k < j; ++k) | 枚举最后一次合并的分界:左半 a[i..k]、右半 a[k+1..j]。k 不能取到 j,否则右半为空。 |
+ pre[j] - pre[i - 1] | 本次得分 = 这一段的总和,用前缀和一次算出。写两层求和会把 O(n³) 变成 O(n⁴)。 |
答案取 n 个区间 | 环有 n 种断法,对应 n 个长度为 n 的区间,答案在这 n 个里取最优。 |
#include <bits/stdc++.h>
using namespace std;
long long pre[10], f[10][10];
int main() {
int a[4] = {0, 4, 5, 9}; // 三堆:4 5 9
for (int i = 1; i <= 3; ++i) pre[i] = pre[i - 1] + a[i];
for (int len = 2; len <= 3; ++len)
for (int i = 1; i + len - 1 <= 3; ++i) {
int j = i + len - 1;
f[i][j] = LLONG_MAX;
for (int k = i; k < j; ++k)
f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + pre[j] - pre[i - 1]);
}
cout << f[1][3] << "\n"; // 27:先 4+5=9,再 9+9=18
return 0;
}长度只枚举到 n,不要到 2n:长度大于 n 的区间在环上没有意义,还会把数组写越界。
求最小值的那张表初值要是 INF(memset 0x3f),求最大值的要是 0。两张表不能共用初值。
五道题写完,把下面这张单子对着自己的提交记录看一遍。每一条都写清了症状,症状能对上,就不用再猜了。
单调队列存值不存下标 → WA。A 题 q[] 里存 a[i] 的值,就没法判断队头是否滑出窗口,只能靠“值相等”猜,一遇到重复数字全错。
h <= t 写成 h < t → WA 或 RE。只剩一个元素时被判成空队列,q[h] 读到的是过期下标,输出一个根本不在窗口里的数。
窗口左端点写成 i−k → WA。窗口是 [i−k+1, i],差这一格会让每个窗口多留一个过期元素,最小值普遍偏小、最大值普遍偏大。
LIS 的比较写成 ≤ → WA。B 题要求严格递增,写 a[j] <= a[i] 时,两个同身高的同学会被串进同一条上升链。样例前两个人身高都是 186,答案会小 1。
B 题把 n − best 写成 best → WA。题目问的是“出列几人”,best 却是“留下几人”。样例里 n = 8、best = 4,两种写法都能过样例——这就是为什么必须自己再造一组数据:n = 5、身高 1 2 1 2 1 时 best = 3 而答案是 2,一眼就露。
二分中点用了下取整 → TLE。C 题求“最大可行值”,mid 必须上取整;写成 (lo+hi)/2 配 lo = mid,当 hi = lo+1 时 mid 恒等于 lo,区间不再缩小,程序空转到超时。
check 漏了最后一跳 → WA。C 题只判石头与石头之间、忘了 L − last,样例能过,错在“最后一段太短”的数据上。
逆序对答案用 int → WA 成负数。D 题 n = 5×10⁵ 全逆序时答案 ≈ 1.25×10¹¹,int 上限才 2.1×10⁹,溢出后输出一个负数。
树状数组下标从 0 开始 → TLE。lowbit(0) = 0,add(0) 里 i += 0 原地踏步,死循环。
E 题 f 初值设成 0 → WA。求最小值的表如果不 memset 成 INF,min 永远取到 0,输出 0;而求最大值的表初值又必须是 0,两张表的初值不能共用。
E 题 i + len − 1 越界 → RE。数组只开到 205 而循环边界写错,越界写的可能是另一个数组,表现为“样例全过、一交就崩”。
关掉页面,开一个空文件,从 #include <bits/stdc++.h> 开始,给 A 题(滑动窗口)掐 22 分钟写完并提交。
计时前先在纸上写三行:队列里存什么、队尾弹出的条件、队头过期的条件。三行写对再动手。
改一改:把窗口长度改成“每一步都不同”(第 i 步的窗口是 [i − kᵢ + 1, i]),单调队列还成立吗?成立,只是过期判断要跟着 kᵢ 走。
再改一改:如果要求每个窗口的中位数,单调队列还够用吗?把答案写下来,第 49 节的对顶堆会回答这道题。
一级:写暴力。A 题每个窗口重扫一遍是 O(nk),小数据点能拿分;C 题从小到大枚举答案 d,每个 d 用 O(n) 判断,也是能过的暴力。有分的代码才叫代码。
二级:换实现。D 题想不出树状数组,就用归并排序统计逆序对——O(n log n) 一样能过,第 46 节的思路本来就是从归并里长出来的。
三级:只写输入输出。哪怕答案是猜的,也要把格式写对,保留“万一”的分数。
绝不做的事:在一道题上耗超过 45 分钟。本场五题分值相同,耗死一题等于放弃四题。
下面是正式的五道题。先掐表自己做,卡满单题用时再看提示;提示看完还没思路,再点开思路;五题全做完之后再点开代码对照——顺序反了,这场模拟赛就白考了。
| 题号 | 题目 | 难度 | 建议用时 |
|---|---|---|---|
| A | 滑动窗口 /【模板】单调队列 | 普及/提高- | 22 分钟 |
| B | 合唱队形 | 普及/提高- | 23 分钟 |
| C | 跳石头 | 普及/提高- | 28 分钟 |
| D | 逆序对 | 普及/提高- | 30 分钟 |
| E | 石子合并 | 普及/提高- | 32 分钟 |
有一个长为 n 的序列 a,以及一个大小为 k 的窗口。窗口从序列最左端开始,每次向右滑动一个单位,直到右端到达序列末尾。对每一个位置上的窗口,求出窗口内元素的最小值和最大值。
输入格式:第一行两个正整数 n、k(1 ≤ k ≤ n ≤ 10⁶)。第二行 n 个整数,表示序列 a(绝对值不超过 10⁹)。
输出格式:共两行。第一行为每个窗口的最小值,第二行为每个窗口的最大值,同一行的数字之间用一个空格隔开。
样例输入8 3
1 3 -1 -3 5 3 6 7
样例输出-1 -3 -3 -3 3 3
3 3 5 5 6 7
先估一下暴力:n 和 k 都可能到 10⁶,每个窗口重扫一遍是 O(nk),最坏 10¹²,稳 TLE。
换角度看:窗口右移一格,只进来一个新数、只出去一个旧数。哪些数从此再也不可能成为最小值?
如果新来的数比当前候选里最后一个还小,那个候选在过期之前都不可能成为答案——因为新数既更小、又活得更久。
维护一个下标队列 q,保证 q 里的下标递增、对应的 a 值也递增。此时队头 a[q[h]] 就是当前窗口的最小值。
每步三件事:① 新元素入队前,把队尾所有“值 ≥ 新元素”的下标弹掉——它们既不如新元素小、又比新元素先过期,永远没机会了;② 把 i 放进队尾;③ 检查队头是否还在窗口 [i−k+1, i] 里,不在就弹掉。
正确性:队列里值单调递增,所以队头是整个队列的最小值;队头一定在窗口内(过期就弹了)。被弹掉的下标要么值更大且更靠左,要么已经过期,都不可能成为任何后续窗口的答案。
复杂度:每个下标最多进队一次、出队一次,while 的总执行次数不超过 n,整体 O(n)。
最大值完全同构:把队尾弹出条件从 a[q[t]] ≥ a[i] 改成 a[q[t]] ≤ a[i],别的一个字都不改。
输出细节:前 k−1 步窗口没填满,不输出;行末只换行,不加多余空格。
#include <bits/stdc++.h>
using namespace std;
int n, k;
int a[1000005];
int q[1000005]; // 手写双端队列,存的是下标
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
for (int i = 1; i <= n; ++i) cin >> a[i];
// ---- 最小值:队列里的下标对应的 a 值严格递增,队头即窗口最小值 ----
int h = 1, t = 0; // h > t 表示队列为空
for (int i = 1; i <= n; ++i) {
while (h <= t && a[q[t]] >= a[i]) --t; // 队尾比新来的大:它这辈子都比不过新来的
q[++t] = i;
if (q[h] < i - k + 1) ++h; // 队头已经滑出窗口左端
if (i >= k) cout << a[q[h]] << (i == n ? '\n' : ' ');
}
// ---- 最大值:同一个骨架,只把比较方向反过来 ----
h = 1; t = 0;
for (int i = 1; i <= n; ++i) {
while (h <= t && a[q[t]] <= a[i]) --t;
q[++t] = i;
if (q[h] < i - k + 1) ++h;
if (i >= k) cout << a[q[h]] << (i == n ? '\n' : ' ');
}
return 0;
}| 片段 | 作用 |
|---|---|
int q[1000005]; | 手写队列,不比 STL 的 deque 慢,还省掉了迭代器和内存分配。n ≤ 10⁶,开 1000005。 |
int h = 1, t = 0; | 队头指针 h、队尾指针 t,初始 h > t 表示空队列。用 1 开始,方便和 a 的下标对齐。 |
while (h <= t && a[q[t]] >= a[i]) --t; | 弹掉队尾所有“值比新元素大”的下标。它们更靠左(先过期)、又更大(更差),被新元素彻底支配。求最大值时把 >= 改成 <=。 |
q[++t] = i; | 存下标,不存值。存了值就无法判断队头有没有滑出窗口。 |
if (q[h] < i - k + 1) ++h; | 窗口是 [i−k+1, i]。每步只可能有一个下标过期,所以用 if 就够。 |
if (i >= k) | 前 k−1 步窗口还没填满,此时输出的是“不足 k 个元素”的答案,必须跳过。 |
cout << a[q[h]] << (i == n ? '\n' : ' '); | 数字之间一个空格;最后一个窗口后面直接换行,不多输出行末空格。两行的写法完全一样。 |
h = 1; t = 0; 第二遍重开 | 两个方向必须各自清空队列。不清空的话,最大值那一遍会读到最小值遗留的下标,结果全错。 |
n 位同学站成一排,音乐老师要请其中的 n−k 位同学出列,使剩下的 k 位同学排成“合唱队形”。合唱队形指存在一个“顶峰”位置 i,使身高满足 t₁ < t₂ < … < tᵢ > tᵢ₊₁ > … > t_k。求最少需要几位同学出列。
输入格式:第一行一个整数 n(2 ≤ n ≤ 100)。第二行 n 个整数,第 i 个表示第 i 位同学的身高 tᵢ(130 ≤ tᵢ ≤ 230)。
输出格式:一个整数,即最少需要出列的人数。
样例输入8
186 186 150 200 160 130 197 220
样例输出4
样例解释:留下 186、200、160、130 这 4 位同学即可排成合唱队形,其余 4 人出列。
“留下的人”必须是先升后降的。形状只由一件事决定:谁是那个最高的人。
枚举顶峰 i 之后,左右两段互相独立:左边是“以 i 结尾的最长上升子序列”,右边是“以 i 开头的最长下降子序列”。
n ≤ 100,所以 O(n²) 的朴素 LIS 完全够用,不要上 O(n log n),那样只是给自己增加写错的机会。
答案 = n − (能留下的最多人数)。所以问题变成“最多能留下几人”。
枚举顶峰 i。左边必须以 i 结尾且严格递增,长度就是 f[i] = 以 i 结尾的最长上升子序列;右边必须以 i 开头且严格递减,长度是 g[i] = 以 i 开头的最长下降子序列。
以 i 为顶峰时的最大保留人数 = f[i] + g[i] − 1,减 1 是因为顶峰自己被两段都算了一次。
f 从左往右推:f[i] = 1 + max{ f[j] : j < i 且 a[j] < a[i] },没有合法的 j 时 f[i] = 1。
g 从右往左推,转移条件同样是 a[j] < a[i]:g[i] = 1 + max{ g[j] : j > i 且 a[j] < a[i] }。
两遍都是 O(n²),总 O(n²) = 10⁴ 次操作。最后输出 n − max(f[i]+g[i]−1)。
#include <bits/stdc++.h>
using namespace std;
int n, a[105];
int f[105], g[105]; // f:以 i 结尾的最长上升长度;g:以 i 开头的最长下降长度
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
// 左半边:以 i 结尾的最长上升子序列
for (int i = 1; i <= n; ++i) {
f[i] = 1; // 至少包含自己
for (int j = 1; j < i; ++j)
if (a[j] < a[i]) f[i] = max(f[i], f[j] + 1); // 严格递增:不能取等
}
// 右半边:以 i 开头的最长下降子序列,从右往左推
for (int i = n; i >= 1; --i) {
g[i] = 1;
for (int j = i + 1; j <= n; ++j)
if (a[j] < a[i]) g[i] = max(g[i], g[j] + 1);
}
int best = 0; // 能留下的最多人数
for (int i = 1; i <= n; ++i)
best = max(best, f[i] + g[i] - 1); // 顶峰被算两次,减 1
cout << n - best << "\n"; // 问的是出列人数,不是留下人数
return 0;
}| 片段 | 作用 |
|---|---|
f[i] = 1; | 初始化为 1:即使前面没有比自己矮的人,自己也能单独构成长度 1 的上升段。 |
if (a[j] < a[i]) | 严格小于。写成 <= 时,两个同身高的同学会被接进同一条上升链,样例前两人身高都是 186,答案会偏小。 |
f[i] = max(f[i], f[j] + 1); | 枚举 i 前面所有更矮的人,取最长的那个再接上自己。这就是 LIS 的 O(n²) 转移。 |
for (int i = n; i >= 1; --i) | 求“以 i 开头”的下降段要从右往左推:算 g[i] 时,它右边所有 g[j] 必须已经算好。 |
if (a[j] < a[i]) g[i] = ... | “往右走必须更矮”等价于“右边的 a[j] < a[i]”,条件和左边一模一样,只是方向相反。 |
f[i] + g[i] - 1 | 两段在顶峰 i 处相交,i 被算了两次,必须减 1。忘了减,答案会少 1,样例输出变成 3。 |
n - best | 题目问的是“出列几人”。这一步最容易漏,而样例里 n−best = 4 恰好等于 best,所以漏了也能过样例——必须自己造数据验。 |
cout << n - best << "\n"; | 只输出一个整数加换行。 |
比赛在一条笔直的河道中进行,河道中有 N 块岩石(不含起点与终点)。选手从起点出发,每一步跳到相邻的岩石上,直到到达终点。组委会至多可以移走 M 块岩石(起点和终点不能移走),要求移走之后,选手所有跳跃距离中的最小值尽可能大。求这个最大可能的最短跳跃距离。
输入格式:第一行三个整数 L、N、M,分别表示起点到终点的距离、岩石数、至多移走的岩石数;接下来 N 行,每行一个整数 Dᵢ,表示第 i 块岩石到起点的距离,按从小到大给出。(0 ≤ M ≤ N ≤ 50000,0 < L ≤ 10⁹)
输出格式:一个整数,即最短跳跃距离的最大值。
样例输入25 5 2
2
11
14
17
21
样例输出4
“最大值最小 / 最小值最大”这类字眼一出现,先想二分答案。这里问的是“最短跳跃距离的最大值”。
固定一个 d,问:能不能只移走不超过 M 块石头,让每一跳都 ≥ d?这个判断比原问题好想得多。
判断的时候从左往右贪心:能跳到就跳,跳不到就把中间那块石头搬走。搬走一块之后,跨度会变大还是变小?
二分答案 d:判定“最短跳跃距离 ≥ d”是否可行。d 越大越难满足,单调性成立。
check(d):last 记录上一块“保留下来的落脚点”坐标(起点是 0),从左往右扫岩石。若 a[i] − last < d,这跳太短,只能把第 i 块搬走(cnt++),last 不变;否则保留它,last = a[i]。
扫完还要检查最后一跳 L − last:若它 < d,就把最后保留的那块石头搬走(不是搬终点),这样“上一落脚点 → L”的距离只会更大,一定也 ≥ d。
为什么贪心最优:能保留就保留,保留只会让后续的距离更容易够(last 更靠右)。提前搬走一块只会让跨度更大、消耗更多次数,不可能更优。
判定条件是 cnt ≤ M:不超过 M 块就合法,题目说的是“至多移走 M 块”。
二分区间 [1, L]:答案至少 1,至多 L(一块石头都不留时就是 L)。求最大可行值,中点要上取整。
复杂度 O(N log L) ≈ 5×10⁴ × 30 = 1.5×10⁶,稳过。
#include <bits/stdc++.h>
using namespace std;
int L, n, m;
int a[50005]; // a[1..n] 是岩石坐标;起点 0 由 last 的初值表示
// 判定:能不能只移走 ≤ m 块石头,使每一跳都 ≥ d
bool check(int d) {
int cnt = 0, last = 0; // last:上一个保留的落脚点
for (int i = 1; i <= n; ++i) {
if (a[i] - last < d) ++cnt; // 这跳太短,只能搬走第 i 块
else last = a[i]; // 跳得到,保留它
}
if (L - last < d) ++cnt; // 最后一跳不够长:搬走 last 这块(不是终点)
return cnt <= m; // “至多 m 块”,不是“恰好 m 块”
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> L >> n >> m;
for (int i = 1; i <= n; ++i) cin >> a[i];
int lo = 1, hi = L; // 答案至少 1,至多 L
while (lo < hi) {
int mid = lo + (hi - lo + 1) / 2; // 上取整:求最大可行值必须这么写
if (check(mid)) lo = mid; // 可行,试着更大
else hi = mid - 1; // 不可行,必须更小
}
cout << lo << "\n";
return 0;
}| 片段 | 作用 |
|---|---|
if (a[i] - last < d) ++cnt; | 这一跳到上一落脚点不足 d,唯一办法是把第 i 块搬走。用 < 不用 <=:距离恰好等于 d 是合法的。 |
else last = a[i]; | 跳得到就落脚。贪心的全部内容就这一行:保留永远不比搬走差。 |
if (L - last < d) ++cnt; | 最后一跳也要判。这里搬走的是 last(上一块保留的石头),搬走之后“更早的落脚点 → L”只会更远,一定也 ≥ d。 |
return cnt <= m; | 题目说“至多移走 M 块”。写成 == m 会在“不用搬那么多”的数据上判不可行。 |
int lo = 1, hi = L; | 下界 1:任意两块石头位置不同,最短跳至少 1。上界 L:一块石头都不留就是 L。写 hi = 1e9 也能过,但 hi = L 更贴题意。 |
mid = lo + (hi - lo + 1) / 2 | 上取整。求最大可行值时,区间只剩两个元素时 mid 必须取右边那个,否则 lo = mid 会让区间卡死 → TLE。 |
if (check(mid)) lo = mid; | 可行就把下界提到 mid(mid 本身可能正是答案,不能写 mid+1)。 |
else hi = mid - 1; | 不可行就砍掉 mid 及其右边。 |
对于给定的一段正整数序列,逆序对就是序列中满足 aᵢ > aⱼ 且 i < j 的有序对 (i, j)。给定一个长度为 n 的序列,求它的逆序对数目。注意序列中可能有重复数字。
输入格式:第一行一个整数 n(n ≤ 5×10⁵)。第二行 n 个整数,表示给定的序列,每个数字不超过 10⁹。
输出格式:一个整数,即逆序对的数目。
样例输入6
5 4 2 6 3 1
样例输出11
n = 5×10⁵,枚举 (i, j) 是 1.25×10¹¹ 对,稳 TLE。目标复杂度 O(n log n)。
换个计数角度:对每个 j,数一数它左边有多少个数比 aⱼ 大,全部加起来就是答案。
“左边有多少个数比它小”这种前缀计数,值域又只有 5×10⁵ 个不同值——把 10⁹ 压成排名,就能用树状数组一次 log 查出来。
逆序对总数 = 对每个位置 i,统计“右边比 aᵢ 小的元素个数”,全部累加。
值域到 10⁹,桶开不下。先离散化:把 a 复制成 b,排序去重,用 lower_bound 把每个 aᵢ 换成排名,排名范围 1..sz,sz ≤ n ≤ 5×10⁵。
从右往左扫。树状数组 t 里记的是“某个排名已经出现过几次”。扫到 aᵢ 时,排名是 r,则 sum(r−1) 就是右边比它小的个数,直接加进答案;然后把 r 加进树状数组。
为什么用 r−1 而不是 r:逆序对要求严格大于,相等元素不能算。排名相同意味着值相等,必须排除。
正确性:每个逆序对 (i, j) 在扫描到 i 时被恰好数到一次——那时 j 已经进树状数组,且排名更小。
复杂度 O(n log n):离散化 O(n log n),每次查询和修改 O(log n),总计约 5×10⁵ × 19 × 2 次操作。
答案上界:n(n−1)/2 ≈ 1.25×10¹¹,必须 long long。
#include <bits/stdc++.h>
using namespace std;
int n;
int a[500005], b[500005]; // b 是排序去重用的副本
int t[500005]; // 树状数组:t[i] 记录“排名 i 出现了几次”
int sz; // 排名上界 = 不同值的个数
int lowbit(int x) { return x & -x; }
void add(int i) {
for (; i <= sz; i += lowbit(i)) ++t[i];
}
int sum(int i) { // 排名 ≤ i 的元素个数
int s = 0;
for (; i > 0; i -= lowbit(i)) s += t[i];
return s;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; ++i) { cin >> a[i]; b[i] = a[i]; }
// 离散化:排序 + 去重,把值换成排名
sort(b + 1, b + n + 1);
sz = unique(b + 1, b + n + 1) - (b + 1);
long long ans = 0; // 逆序对最多 1.25×10¹¹,int 装不下
for (int i = n; i >= 1; --i) { // 从右往左,保证树状数组里只有 i 右边的数
int r = lower_bound(b + 1, b + sz + 1, a[i]) - b; // a[i] 的排名
ans += sum(r - 1); // 右边严格小于 a[i] 的个数
add(r);
}
cout << ans << "\n";
return 0;
}| 片段 | 作用 |
|---|---|
b[i] = a[i]; | 复制一份再排序,a 本身不能动:后面扫的时候还要按原下标取值。 |
unique(b + 1, b + n + 1) - (b + 1) | 去重后不同值的个数。它不删除元素,只是把重复的搬到末尾,返回值是去重区间的尾后指针。 |
lower_bound(b + 1, b + sz + 1, a[i]) - b | 第一个 ≥ a[i] 的位置,就是从 1 开始的排名。右端点是 sz+1,不是 n+1。 |
for (int i = n; i >= 1; --i) | 倒着扫:扫到 i 时,树状数组里装的正是 a[i+1..n],所以 sum(r−1) 天然就是“右边比它小的个数”。 |
ans += sum(r - 1); | 用 r−1 排除相等元素。写成 sum(r) 会把值相同的一对也当成逆序对。 |
add(r); | 把这个排名计入树状数组,供更左边更小的下标查询。别忘了这一句,否则答案恒为 0。 |
long long ans | n = 5×10⁵ 且完全逆序时答案 = n(n−1)/2 ≈ 1.25×10¹¹,int 上限 2.1×10⁹,溢出后是负数。 |
int lowbit(int x) { return x & -x; } | x 与它的相反数按位与,留下最低位的 1。下标必须 ≥ 1,否则 lowbit(0)=0 造成死循环。 |
在一个圆形操场的四周摆放 N 堆石子,现要将石子有次序地合并成一堆。规定每次只能选相邻的 2 堆合并成新的一堆,并把新堆的石子数记为本次合并的得分。求把 N 堆合并成一堆的最小总得分和最大总得分。
输入格式:第一行一个整数 N(N ≤ 100);第二行 N 个整数,依次表示每堆石子的个数(正整数)。
输出格式:共两行,第一行为最小得分,第二行为最大得分。
样例输入4
4 5 9 4
样例输出43
54
样例解释:最小方案先合并 4+4=8(得 8 分),再 8+5=13(13 分),再 13+9=22(22 分),共 43。
先做一条链上的版本:n 堆排成一行,每次合并相邻两堆。把“最后一步”想清楚——它一定是把某个前缀段和某个后缀段合并。
于是需要知道“把 a[i..j] 合并成一堆”的最优得分。这就是区间 DP:先算小区间,再用小区间拼出大区间。
环怎么处理?一种断法对应一种走法,有 n 种断法。有没有办法不写 n 遍,一遍扫完?
断环成链:把数组复制一份接在后面,得到 a[1..2n]。环上任意一种“从哪断开”的合并方案,都对应 2n 链上一个长度为 n 的连续区间。答案在这 n 个区间里取最优。
状态:f[i][j] = 把 a[i..j] 合并成一堆的最小得分;g[i][j] 同理求最大。
转移:最后一次合并必然把 a[i..k] 与 a[k+1..j] 两堆合成一堆,本次得分 = 这一段总和。f[i][j] = min over k∈[i, j−1] of f[i][k] + f[k+1][j] + (pre[j] − pre[i−1])。
顺序:按区间长度 len 从小到大枚举。长度 1 的区间不需要合并,得分 0。不能用“从 i 推到 j”的方式,因为 f[i][j] 依赖的所有子区间长度都比它短。
边界:f[i][i] = 0,其余 f 初值设成 INF;g 全部初值 0(最大值的下界就是 0)。
答案:对 i = 1..n 取 f[i][i+n−1] 的最小值、g[i][i+n−1] 的最大值。
复杂度 O(n³):区间 O(n²) 个(实际是 2n 个起点 × n 种长度),每个枚举 O(n) 个分界点,n = 100 时约 2×10⁶ 次。
#include <bits/stdc++.h>
using namespace std;
int n;
long long a[205], pre[205];
long long f[205][205], g[205][205]; // f:最小得分,g:最大得分
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; ++i) { cin >> a[i]; a[i + n] = a[i]; } // 断环成链
for (int i = 1; i <= 2 * n; ++i) pre[i] = pre[i - 1] + a[i]; // 前缀和,O(1) 取段和
memset(f, 0x3f, sizeof(f)); // 求最小值:初值设成极大
for (int i = 1; i <= 2 * n; ++i) f[i][i] = 0; // 一堆不用合并,得分 0
for (int len = 2; len <= n; ++len) // 区间长度,先短后长
for (int i = 1; i + len - 1 <= 2 * n; ++i) { // 左端点
int j = i + len - 1;
long long s = pre[j] - pre[i - 1]; // 本次合并的得分
for (int k = i; k < j; ++k) { // 最后一次合并的分界
f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s);
g[i][j] = max(g[i][j], g[i][k] + g[k + 1][j] + s);
}
}
long long mn = LLONG_MAX, mx = 0;
for (int i = 1; i <= n; ++i) { // n 种断法
mn = min(mn, f[i][i + n - 1]);
mx = max(mx, g[i][i + n - 1]);
}
cout << mn << "\n" << mx << "\n";
return 0;
}| 片段 | 作用 |
|---|---|
a[i + n] = a[i]; | 把数组复制一份接在后面。环上每一种断法,对应链上一个长度为 n 的连续区间。 |
pre[i] = pre[i - 1] + a[i]; | 前缀和。段和 pre[j]−pre[i−1] 是 O(1),这一步把 O(n⁴) 的朴素写法压到 O(n³)。 |
memset(f, 0x3f, sizeof(f)); | 把 f 的每个字节设成 0x3f,得到一个约 4.5×10¹⁸ 的大数,代表“还没算 / 不可达”。 |
for (int i = 1; i <= 2 * n; ++i) f[i][i] = 0; | 长度为 1 的区间是递归的底:一堆石子不用合并,得分 0。漏了这一行,所有答案都会带上 INF。 |
for (int len = 2; len <= n; ++len) | 区间 DP 的方向:长度从小到大。长度超过 n 的区间在环上没有意义。 |
i + len - 1 <= 2 * n | 右端点不能越过 2n。写成 i <= n 会让短区间的右端点越过边界,越界写数组。 |
for (int k = i; k < j; ++k) | 枚举最后一次合并的分界点。k 从 i 到 j−1,保证左右两段都非空。 |
+ s | 本次合并的得分 = 合并后这一堆的石子数 = 这一段的总和。三项缺一不可。 |
mn = min(mn, f[i][i + n - 1]); | n 种断法里取最优。注意区间长度是 n,右端点是 i+n−1。 |