第 16~19 节讲的是四件事:贪心决定“选什么”,排序把选择的标准写成一行 cmp,前缀和 / 差分把“反复算一段区间”换成两三个端点的操作,双指针把“枚举所有区间”压成一条只会右移的窗口。四件工具共享同一个前提:后面要用的信息,能压成一个数。压不成,就只剩 DP 和搜索。
| 节 | 内容 | 一句话判据 | 判据不成立时 |
|---|---|---|---|
| 16 | 贪心入门:交换论证与排序贪心 | 总代价能写成 Σ(值 × 位置系数),交换相邻两项不变差 | 按性价比拿 0/1 背包,答案错 |
| 17 | 贪心进阶:区间问题与堆 | 区间调度按右端点升序;每次要取最小的两个就上堆 | 按左端点排,选中的区间少一半 |
| 18 | 前缀和与差分 | 多次查询区间和用 pre;多次区间加、最后统一输出用 d | 混着用,复杂度降不下来 |
| 19 | 双指针与滑动窗口 | 右端点右移后,左端点只会右移、不回头 | 窗口没有单调性,边缩边漏解 |
回顾章不讲新知识。下面先看一道能同时走通“前缀和”和“贪心”两条路的题,再用 5 道黄题把四件工具各打一遍——每题都给完整题面、提示、思路、代码和易错点。
给出一个长度为 n 的序列 a,选出其中连续且非空的一段,使这段的和最大,输出这个最大和。
输入格式:第一行一个整数 n(1 ≤ n ≤ 2×10⁵);第二行 n 个整数 a_i(|a_i| ≤ 10⁴)。
输出格式:一行一个整数,即最大子段和。
为什么拿它当例题:它表面是前缀和题,本质是贪心题。会写 pre 表的人很多,能说清“为什么只需要维护一个最小值”的人不多——后者才是第 18 节真正想留下的东西。而且它不到 20 行,改一个初值就能造出三种 WA,是最好的验错材料。
| 组成 | 含义 / 格式要求 |
|---|---|
pre[i] | 前 i 个数的和,是累计量而不是原数组。区间和靠两个前缀相减得到。 |
pre[r] - pre[l-1] | 区间 [l, r] 的和。一次减法顶替一次长度为 len 的循环。 |
minpre | 已经扫过的所有前缀里的最小值(含 pre[0] = 0)。固定右端点 r 时,减数越小答案越大。 |
更新顺序 | 先用旧的 minpre 算答案,再把 pre[i] 并进 minpre。顺序反了就等于允许空区间。 |
ans 初值 | 必须取负无穷(LLONG_MIN)。子段非空,全负数时答案是“最大的那个负数”,不是 0。 |
量级 | n = 2×10⁵、|a| = 10⁴ 时 |pre| 可达 2×10⁹,已经贴住 int 上限,用 long long。 |
long long pre = 0, minpre = 0, ans = LLONG_MIN; // minpre 的初值 0 就是 pre[0]
for (int i = 1; i <= n; ++i) {
pre += a[i]; // pre 此刻等于 pre[i]
ans = max(ans, pre - minpre); // ① 先算答案,保证区间非空
minpre = min(minpre, pre); // ② 再把 pre[i] 并进历史最小值
}
// a = {2, -4, 3, -1, 2, -4, 3} → ans = 4,来自 3 + (-1) + 2把 ①② 换序:先 minpre = min(minpre, pre) 再算答案,减数可能取到 pre[i] 本身,等于允许空段。症状是全负数数据输出 0。
存了 pre 数组却每次从 1 到 r 找最小值,复杂度退回 O(n²)——minpre 的意义就是“顺手记下过去的最好值”。
| 组成 | 含义 / 格式要求 |
|---|---|
d[i] | 相邻两项的变化量。区间加 z 只需要 d[l] += z、d[r+1] -= z 两次单点修改。 |
还原 | 对 d 求一遍前缀和才得到真实值。d 本身不是答案,它只是记账。 |
正部之和 | 把一段序列“削平”(每次让一个连续区间减 1)的最少次数 = d[1] + Σ max(0, d[i] − d[i−1]),也就是所有上升量之和。 |
为什么 | 每次操作最多消掉一个单位的高度差,所以“上升量之和”是下界;按“每逢上升就新开几次操作”的策略恰好取到这个下界,于是它也是上界。 |
数组大小 | 要写 d[r+1],而 r 可以等于 n,所以开 n+2。 |
流式写法 | 只求和时不用存数组:读一个数、与上一个比较,O(1) 空间。 |
// 铺设道路:n 段深度 d[i],每次把一段连续区间每段减 1,求最少次数
int prev = 0; long long ans = 0; // prev 相当于 d[0] = 0
for (int i = 1; i <= n; ++i) {
int d; cin >> d;
if (d > prev) ans += d - prev; // 只累加“上升”的部分
prev = d;
}
// 4 3 2 5 3 5 → 4 + 3 + 2 = 9算成 Σ|d[i] − d[i−1]| 会把下降量也算进去,答案偏大。
忘掉 prev 的初值 0,第一段就少了 d[1] 次操作。
| 组成 | 含义 / 格式要求 |
|---|---|
三个模板参数 | 元素类型、底层容器 vector、比较器 greater<int>(越小越优先)。少写后两个就是大根堆,行为正好相反。 |
pq.push(x) | O(log n) 插入一个元素。 |
pq.top() | O(1) 取出堆顶,即当前最小值。只读不删。 |
pq.pop() | O(log n) 删除堆顶,没有返回值:必须 top 之后再 pop。 |
pq.size() / empty() | 元素个数 / 是否为空。size() 返回无符号数,与 int 比较时先转 int。 |
用在哪 | 每次决策都要“取当前最小的一两个”——合并果子、哈夫曼编码、多路归并、以及第 17 节里“已经选过、但还能反悔”的贪心。 |
priority_queue<int, vector<int>, greater<int>> pq; // 小根堆
pq.push(5); pq.push(1); pq.push(3);
while (pq.size() >= 2) {
int a = pq.top(); pq.pop(); // 最小
int b = pq.top(); pq.pop(); // 次小
pq.push(a + b); // 合并后的新堆放回去
}写成 priority_queue<int> 就是大根堆:合并果子会每次合并最大的两堆,答案偏大。
top 之后忘了 pop,循环里永远取到同一个值——不是死循环就是答案错。
| 组成 | 含义 / 格式要求 |
|---|---|
返回值 bool | 只回答“a 该不该排在 b 前面”,表达的是优先级,不是“大小”。 |
const Seg & | 引用传参,不拷贝也不修改。值传递每次比较多拷一份结构体。 |
主键 | 区间调度按右端点升序,不是左端点。改这一个符号就是另一个算法,必须有证明撑着。 |
次键(兜底) | 主键相等时补一条次键,输出顺序才固定可复现。不补不算违反严格弱序(平局都返回 false 是合法的“等价”),只是结果依赖 sort 的内部顺序。 |
严格弱序 | cmp(a, a) 必须为 false;cmp(a,b) 与 cmp(b,a) 不能同时为 true。写成 <= 就破坏了它。 |
struct Seg { int l, r; };
bool cmp(const Seg &x, const Seg &y) {
if (x.r != y.r) return x.r < y.r; // 主键:结束越早越优先
return x.l < y.l; // 兜底:结束相同,开始早的在前
}
sort(v.begin(), v.end(), cmp);
int cnt = 0, last = -1; // last:上一场结束的时刻
for (auto &s : v) if (s.l >= last) { ++cnt; last = s.r; }
// 0 2 / 2 4 / 1 3 → 2把 cmp 写成 x.r <= y.r(平局也返回 true)才真的破坏严格弱序:sort 可能越界写内存,症状是 RE 或随机 WA。
cmp 里不要改数据、不要输出:sort 会调用它 O(n log n) 次。
路 1:暴力。枚举左右端点 l ≤ r,内层求和,O(n³);前缀和预处理后变 O(n²)。n = 2×10⁵ 时约 2×10¹⁰ 次加法,必然 TLE。
路 2:前缀和改写。区间和 = pre[r] − pre[l−1],于是答案 = max over r of ( pre[r] − min{ pre[j] : 0 ≤ j < r } )。这一步把“枚举两个端点”换成了“枚举一个端点 + 一个历史最小值”。
路 3:贪心维护。固定 r 时减数越小答案越大;而“r 之前的最小前缀”只由已经读过的数决定,与后面读什么无关——它不消耗未来资源、没有后效性,所以一遍扫描就够:O(n) 时间、O(1) 空间。
long long pre = 0, minpre = 0, ans = LLONG_MIN;
for (int i = 1; i <= n; ++i) {
pre += x; // 边读边累加,连数组都不用存
ans = max(ans, pre - minpre);
minpre = min(minpre, pre);
}两个初值决定 AC 还是 WA。① ans = LLONG_MIN:数组全负时答案是“最大的那个负数”,写成 0 会输出 0(空子段,非法)。② minpre = 0 代表 pre[0],让“从第一个数开始”的子段能被取到;再配合“先算答案、后更新 minpre”,就永远不会取到空区间。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
ll pre = 0; // 当前前缀和 pre[i]
ll minpre = 0; // 已经扫过的前缀里的最小值,初值 0 就是 pre[0]
ll ans = LLONG_MIN; // 子段必须非空,所以用负无穷而不是 0
for (int i = 1; i <= n; ++i) {
ll x;
cin >> x;
pre += x; // 读一个数就累加,省掉数组
ans = max(ans, pre - minpre); // ① 先算答案:右端点是 i,左端点 ≤ i
minpre = min(minpre, pre); // ② 再更新历史最小值,供后面的右端点使用
}
cout << ans << "\n";
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
ios::sync_with_stdio(false); | 2×10⁵ 个数谈不上瓶颈,但这是所有竞赛代码的默认动作,别在这种地方养成例外。 |
typedef long long ll; | 下面几个变量的量级都能到 2×10⁹,统一用 ll,读代码时不必再逐个判断类型。 |
ll pre = 0; | pre 就是 pre[i],只是不存整张表。第 18 节的 pre 数组在这里退化成一个变量。 |
ll minpre = 0; | 0 代表 pre[0](前 0 个数之和)。有了它,l = 1 的情况不用特判。 |
ll ans = LLONG_MIN; | −9.22×10¹⁸,比任何可能的答案都小。换成 0 会在全负数据上错。 |
pre += x; | 累加。此时 pre 已经不是“前 i−1 个”,而是“前 i 个”,后面所有判断都用新值。 |
ans = max(ans, pre - minpre); | 以 i 结尾的最大子段和 = pre[i] − (i 之前最小的前缀)。此刻 minpre 还是旧值,所以减出来的一定非空。 |
minpre = min(minpre, pre); | 把当前前缀并入历史最小值。这一句放在答案更新之后,是本题唯一的顺序要求。 |
cout << ans << "\n"; | 只要一个整数加换行。不要输出提示语,也不要在行末留空格。 |
| 题目里的信号 | 该用哪件工具 | 判据 | 复杂度 |
|---|---|---|---|
| 顺序决定总代价(谁先谁后) | 排序贪心 | 交换相邻两项,答案不变差 | O(n log n) |
| 尽量多选互不重叠的区间 | 区间调度 | 按右端点升序,能放就放 | O(n log n) |
| 每次都要取当前最小的两个 | 小根堆 | 需要“反悔”的选择交给堆 | O(n log n) |
| 多次询问区间和 | 前缀和 | 运算可逆(有减法) | O(n) 建表,O(1) 查 |
| 多次区间加,最后统一输出 | 差分 | 一次区间加 = 两个端点改 | O(1) 改,O(n) 还原 |
| 最长 / 最短的满足条件的连续段 | 双指针、滑动窗口 | 右端点右移后左端点不回头 | O(n) |
两场活动能先后参加,条件是“前一场结束 ≤ 后一场开始”。每选一场,代价是这段时间被占用。
结束得越早,留给后面的时间越多——这正是“局部选择不消耗未来资源”。交换论证:设最优解第一场是 x,结束最早的是 e,因为 e 的结束 ≤ x 的结束,把 x 换成 e 后后面只会更宽松,所以存在含 e 的最优解。
反例记一下:按开始时间排序,遇到 (0, 100)、(1, 2)、(3, 4) 会先拿 (0, 100),只得 1 场,正确是 2 场。
代码只有三行:按 (右端点升序、左端点升序) 排序,last = -1,if (l >= last) { ++cnt; last = r; }。
最大子段和把 ans 初始化成 0:数组全负时输出 0。症状是全负数数据 WA、正数数据全过。
先更新 minpre 再算答案:等于允许空区间,全负数据同样输出 0。顺序错和初值错,症状一模一样,所以这两句要一起背。
前缀和 / 区间和用 int:n = 2×10⁵、|a| = 10⁴ 时 |pre| 可达 2×10⁹,只差 7% 就撞上 int 上限,再累加或再乘一次就溢出成负数 → WA。
差分数组只开 n 个:d[r+1] 在 r = n 时写到下标 n+1 → RE 或随机 WA。开 n+2。
区间调度按左端点排序:遇到“开始早、特别长”的区间就把后面全占掉。症状是答案偏小,样例却常常能过。
小根堆写成 priority_queue<int>:本该每次取最短的两段,变成取最长的两段,答案偏大 → WA。
双指针 while 里忘记移动下标,或把窗口条件写成“不满足才缩”:前者死循环 TLE,后者窗口收不紧、答案偏大。
关掉页面,把上面 P1115 的完整代码从零敲一遍,提交后确认样例输出 4。
改一改①:把 ans = LLONG_MIN 改成 ans = 0,输入 5 / -3 -1 -4 -2 -5,看它输出什么——亲手见一次“空子段”的错。
改一改②:把 max 与 min 全反过来(ans = min(...)、minpre 换 maxpre、初值换 LLONG_MAX),求最小子段和。用例题样例 2 -4 3 -1 2 -4 3 应当得到 −4,验证对称性。
改一改③:额外输出最大子段的左右端点。提示:minpre 更新时顺手记下它所在的位置,答案被更新时记下当前的 i;想一想为什么“位置”不能事后补算。
0~10 分钟:只读 5 道题,各写一个关键词(排序?前缀和?窗口?堆?),再按“能立刻写 → 有思路 → 没思路”排序。
10~25 分钟:A 题 P1094。最短的一道,练双指针手感,重点盯 l == r 这个边界。
25~45 分钟:B 题 P2240 与 C 题 P1334。一个考排序键(单价),一个考堆(把“锯”倒着看成“合并”),两题都不超过 35 行。
45~75 分钟:D 题 P5019。先手算 4 3 2 5 3 5 得 9,再把“只累加上升量”写成三行。
75~90 分钟:E 题 P1147。写不动就只核对循环边界、全局变量、行末空格,不写新代码。
| 题 | 编号 | 主要考什么 | 最容易错在哪 |
|---|---|---|---|
| A | P1094 | 排序 + 双指针的配对贪心 | 普及- |
| B | P2240 | 排序贪心:按单价降序取满 | 普及- |
| C | P1334 | 小根堆:把“锯”倒看成“合并” | 写成大根堆;用 int 存累计代价 |
| D | P5019 | 差分的正部之和 | 普及/提高- |
| E | P1147 | 双指针维护连续段的和 | 循环写成 l <= r 输出单数段;找到一段就 break |
答案要 long long 吗?凡是“求和”“乘积”,先按最坏情况估一遍量级。
数组开够了吗?差分要 n+2,计数数组要值域+5,10⁶ 级数组必须放全局(静态区)。
排序键是主键吗?区间题问自己一句:按左端点还是右端点?兜底次键写了吗?
三个初值对不对?minpre = 0、ans = LLONG_MIN、sum = 3, l = 1, r = 2(P1147 的初始窗口)——初值各错一次,就是三次 WA。
输出格式:行末有没有多余空格?多测题有没有清空数组和计数器?
限时 100 分钟。每题先自己写 20~25 分钟;写不出来先看提示,再给自己 10 分钟;还不行才点开思路。每题都独立限时,不要一题耗死。
把 n 件纪念品分成若干组,每组最多两件,且组内价格之和不能超过 w。求最少分成几组。
输入格式:第一行一个整数 w(80 ≤ w ≤ 200),表示每组价格之和的上限;第二行一个整数 n(1 ≤ n ≤ 30000);接下来 n 行,每行一个整数 p_i(5 ≤ p_i ≤ w),表示一件纪念品的价格。
输出格式:一行一个整数,即最少的分组数目。
先排序。最贵的那件最难找到搭档,应该让它第一个被安排掉。
最贵的能配上一件时,配最便宜的那件,还是配一件“刚好装得下”的?想想哪一件更难被安排。
把最便宜的放左端、最贵的放右端,两个下标往中间走,每次至少有一件被安排掉。
贪心策略:排序后让最贵的 p[r] 先决定命运——若能配上最便宜的 p[l](p[l] + p[r] ≤ w),就配成一组;否则 p[r] 单独一组。
交换论证:设最优解里 p[r] 与 x 配对。若 x 不是最便宜的 p[l],因为 p[l] ≤ x,把 x 换成 p[l],和不会超限、组数不变。若最优解里 p[r] 单独一组,而 p[l] 与 y 配对,把这两组合并成 (p[r], p[l]) 和 (y) ,组数同样不变。所以“能配就配最便宜的”一定存在最优解。
双指针实现:l 从左、r 从右。每轮至少出局一件(配成一组出局两件),共 O(n) 轮;总复杂度是排序的 O(n log n)。
只剩一件(l == r)时不能自己配自己,直接单独一组,这也是最容易漏的边界。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int w, n;
cin >> w >> n;
vector<int> p(n);
for (int i = 0; i < n; ++i) cin >> p[i];
sort(p.begin(), p.end()); // 排序后两端才是“最便宜 / 最贵”
int l = 0, r = n - 1, cnt = 0;
while (l <= r) {
if (l == r) { ++cnt; break; } // 只剩一件:单独一组
if (p[l] + p[r] <= w) { ++l; --r; } // 能配就配,两件一起出局
else { --r; } // 配不上,最贵的单独一组
++cnt;
}
cout << cnt << "\n";
return 0;
}| 片段 | 作用 |
|---|---|
sort(p.begin(), p.end()); | 贪心的两个决策对象就在两端:最便宜的、最贵的。排序是全部代价。 |
while (l <= r) | 两个下标相遇即结束。每轮至少出局一件,所以最多 n 轮。 |
if (l == r) { ++cnt; break; } | 区间只剩一件时不能自己和自己配对,直接单独一组;漏掉这一句要么死循环要么少算一组。 |
if (p[l] + p[r] <= w) | 能配就配。连最便宜的都配不上最贵的,说明最贵的和谁都配不上。 |
else { --r; } | 配不上时最贵的只能单独一组,右端点左移一件。 |
++cnt; | 两个分支都消耗一组,统一在分支外计数,避免两处各写一次。 |
p[205]:n 最多 30000,越界 → RE。用 vector 或开 30005。--r,循环不动 → TLE。<= w 写成 < w:和恰好等于上限的组合被拆成两组,答案偏大。有 n 种物品和一个容量为 t 的背包。第 i 种物品重 m_i、总价值 v_i。与 0/1 背包不同,这里的物品可以分割:允许只取其中一部分,按比例拿到对应的价值。求背包能装下的最大总价值。
输入格式:第一行两个整数 n、t(1 ≤ n ≤ 100,1 ≤ t ≤ 1000);接下来 n 行,每行两个整数 m_i、v_i(1 ≤ m_i, v_i ≤ 100)。
输出格式:一行一个实数,表示最大总价值,保留两位小数。
先把“该先装谁”变成一个能算的量:每单位重量值多少钱。
如果用一个单价低的东西挤掉了一部分单价高的,交换之后总价值会变多还是变少?
排序后从前往后拿;遇到装不下的那一种,能拿多少拿多少,然后立刻停。
贪心策略:按单价 v_i / m_i 从大到小排序,依次尽量取满;遇到装不下的那一种,只取剩余容量的那一份,然后结束。
交换论证:设最优解里取了一部分单价较低的物品 B,而单价较高的物品 A 没有取完。把 B 的一份重量换成 A 的一份重量:总重量不变,总价值不减。所以一定存在一个“按单价降序取满”的最优解。
为什么这题能贪、0/1 背包不能:物品可以切开,“取多少”是连续的,交换时总能换出任意小的量;0/1 背包只能整件拿,交换会破坏可行性。这正好落在第 16 节那张判据表的“是否消耗未来资源”上。
实现要点:排序比较用交叉相乘 a.v * b.m > b.v * a.m,不写除法;答案用 double,最后一种物品按重量比例折算。
#include <bits/stdc++.h>
using namespace std;
struct Item { int m, v; };
bool cmp(const Item &a, const Item &b) {
// 单价降序:a.v/a.m > b.v/b.m,交叉相乘,避开浮点误差
return 1LL * a.v * b.m > 1LL * b.v * a.m;
}
int main() {
int n, t;
cin >> n >> t;
vector<Item> a(n);
for (int i = 0; i < n; ++i) cin >> a[i].m >> a[i].v;
sort(a.begin(), a.end(), cmp);
double ans = 0;
for (int i = 0; i < n; ++i) {
if (t >= a[i].m) { // 装得下就整件拿
t -= a[i].m;
ans += a[i].v;
} else { // 只能拿剩余容量的那一份
ans += 1.0 * a[i].v * t / a[i].m;
t = 0;
break; // 背包已满,后面的单价更低,不用再看
}
}
printf("%.2f\n", ans);
return 0;
}| 片段 | 作用 |
|---|---|
bool cmp(const Item &a, const Item &b) | 只比较单价,不改数据。返回 true 表示 a 的单价更高、应排在前面。 |
1LL * a.v * b.m > 1LL * b.v * a.m | 交叉相乘代替除法:两边都是整数,比较精确;写成 a.v/a.m 会把 2/3 和 3/5 都算成 0。 |
if (t >= a[i].m) | 装得下就整件拿——这就是“单价最高优先”的直接体现。 |
ans += 1.0 * a[i].v * t / a[i].m; | 只装一部分:按重量比例折算价值。先乘 1.0 转浮点,否则整数除法把小数截掉。 |
break; | 容量已满,继续循环不会再加东西,反而可能让 t 变成负数。 |
printf("%.2f\n", ans); | 两位小数。一次性格式化输出用它最省事,不受 cout 流状态影响。 |
a.v / a.m > b.v / b.m:整数除法把 2/3 与 3/5 都算成 0,排序结果乱。240 而不是 240.00 → WA。瑞瑞要把一根长木板锯成 n 段,第 i 段长 L_i。木板的初始长度就是这 n 段之和。每次锯开一块木板,代价等于这块木板锯之前的长度,锯完得到两块更短的木板。求锯成这 n 段所需的最小总代价。
输入格式:第一行一个整数 n(1 ≤ n ≤ 20000);接下来 n 行,每行一个整数 L_i(1 ≤ L_i ≤ 50000)。
输出格式:一行一个整数,即最小总代价。
把过程倒过来看:锯是“一块变两块”,倒过来就是“两块并成一块”,代价同样是并完的长度。
倒过来之后,它和 17 节的合并果子是同一道题:每次合并最短的两段。
合并出来的新段还要继续参与合并,所以它必须放回容器里。
反向思考:锯一次,一块长 S 的木板的代价是 S,得到长 a、b 的两块,且 S = a + b;倒过来看,就是“把 a、b 两块并成 S,代价 S = a + b”。两种过程的总代价一一对应。
于是问题等价于:每次合并最短的两段,直到只剩一段,求最小总代价——这就是哈夫曼(合并果子)。第 17 节已经证过:若某次合并的不是最短的两段,换成最短的两段,本次代价变小,而且新段更短,对后续只会更有利。
复杂度:n − 1 次合并,每次两次 pop 一次 push,O(n log n);n = 2×10⁴ 时不到 3×10⁵ 次堆操作。
量级:每段 5×10⁴、n = 2×10⁴ 时累计代价可达 10¹⁰ 量级,堆和答案都用 long long。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
// 小根堆:每次都要取最短的两段
priority_queue<long long, vector<long long>, greater<long long>> pq;
for (int i = 0; i < n; ++i) {
long long x;
cin >> x;
pq.push(x);
}
long long ans = 0;
while (pq.size() >= 2) {
long long a = pq.top(); pq.pop(); // 最短的一段
long long b = pq.top(); pq.pop(); // 次短的一段
ans += a + b; // 合并代价 = 拼接后的长度
pq.push(a + b); // 新段放回去,继续参与合并
}
cout << ans << "\n";
return 0;
}| 片段 | 作用 |
|---|---|
priority_queue<long long, vector<long long>, greater<long long>> pq; | 堆里存段长,用 long long;比较器换成 greater 才是小根堆,否则每次取到的是最长的一段。 |
while (pq.size() >= 2) | n 段要合并 n − 1 次,循环次数正好对上;n = 1 时一次都不做,答案 0。 |
long long a = pq.top(); pq.pop(); | 先取再删,两句不能合成一句:pop() 没有返回值。 |
ans += a + b; | 这次“合并”的代价等于新段长度,也就是原来那次“锯”的代价。 |
pq.push(a + b); | 新段必须回堆,否则下一次取到的还是旧值,答案偏小。 |
一条道路分 n 段,第 i 段下陷深度为 d_i。每次操作可以选一段连续区间 [l, r],把区间内每段的深度都减 1。求把整条道路填平(所有 d_i 变为 0)的最少操作次数。
输入格式:第一行一个整数 n(1 ≤ n ≤ 100000);第二行 n 个整数 d_i(0 ≤ d_i ≤ 10000)。
输出格式:一行一个整数,即最少操作次数。
先想 n = 1:答案是 d_1。再想 n = 2、d = 4 3:答案是多少?和 4 4 比差在哪?
从第 i−1 段走到第 i 段,如果变深了,多出来的那一截必须靠新开的操作去削。
把所有“上升量”加起来:d = 4 3 2 5 3 5 时答案是 9 吗?
换个说法(差分):令 d_0 = 0,答案 = Σ max(0, d_i − d_{i−1}),即所有上升量之和。
下界:每次操作只让“某一段连续区间”各减 1,因此一个上升沿有多高,就至少需要那么多次操作去削平它;把所有上升沿加起来,就是任何方案都逃不掉的下界。
可达:从左往右扫,遇到 d_i > d_{i−1} 就新开 d_i − d_{i−1} 次操作,并让这些操作与前面仍有“余量”的操作并行推进——每个位置都能恰好被削到 0。下界可达,即答案。
实现:连数组都不用开:读一个数与上一个比较,只累加上升量,O(n) 时间、O(1) 空间。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
long long ans = 0; // 答案上限 10^5 × 10^4 = 10^9,long long 更稳
int prev = 0; // 相当于 d[0] = 0
for (int i = 1; i <= n; ++i) {
int d;
cin >> d;
if (d > prev) ans += d - prev; // 只累加“上升”的部分
prev = d;
}
cout << ans << "\n";
return 0;
}| 片段 | 作用 |
|---|---|
int prev = 0; | pre 数组的 pre[0] 在这里换成了 prev 的初值。少了它,第一段就漏掉 d_1 次操作。 |
if (d > prev) ans += d - prev; | 上升量 = 新开操作数。下降时一句都不加:那部分由前面开好的操作顺手削掉。 |
prev = d; | 处理完立刻前移。注意是赋值,不是累加。 |
long long ans | 本题 int 也够(10⁹),但同一份代码换到 d ≤ 10⁹ 就溢出,习惯用 long long。 |
cout << ans << "\n"; | 一个整数加换行,不要输出多余内容。 |
给定一个自然数 M,求出所有至少包含两个数的连续自然数段,使段内所有数之和恰好等于 M。按首数从小到大输出每一段的首数和尾数。
输入格式:一行一个整数 M(10 ≤ M ≤ 2×10⁶)。
输出格式:若干行,每行两个整数,依次为这一段的首数与尾数,按首数升序排列。
固定首数 l,段和随尾数 r 增大而增大——所以对每个 l 至多只有一个 r 能用。
更省事的写法:维护一个连续段 [l, r] 的和。和太小就让右端右移,和太大就让左端右移。
题目要求每段至少两个数,所以 l == r 时即使和恰好是 M 也不能输出。
双指针:维护窗口 [l, r] 的整数和 sum,初始 l = 1、r = 2、sum = 1 + 2 = 3。
sum < M:右端右移一格,sum 加上新的 r。sum > M:左端右移一格,sum 减去旧的 l。sum == M:输出 l、r,再把左端右移,继续找下一段。
为什么 O(M):l 与 r 都只增不减,各自最多走到 M,总移动次数 O(M);M = 2×10⁶ 时不到 10⁷ 次加减,几十毫秒就完。
为什么不会漏:固定 l 时 sum 随 r 严格递增,一旦 sum ≥ M 就没有更长的段适合这个 l,只能右移 l——这就是“窗口单调”的全部依据。
边界:循环写 while (l < r)。l == r 时窗口只剩一个数,题目不允许;而且此时任何以 l 开头的更长的段之和都已超过 M。
#include <bits/stdc++.h>
using namespace std;
int main() {
int M;
cin >> M;
int l = 1, r = 2;
long long sum = 3; // 当前窗口 [l, r] 内所有数之和
while (l < r) { // l == r 表示只剩一个数,题目要求至少两个数
if (sum == M) {
cout << l << " " << r << "\n";
sum -= l;
++l; // 这一段找完,左端右移继续找下一段
} else if (sum < M) {
++r; // 先让 r 前进
sum += r; // 再把新的 r 纳入窗口
} else {
sum -= l; // 和太大,把左端移出窗口
++l;
}
}
return 0;
}| 片段 | 作用 |
|---|---|
int l = 1, r = 2; long long sum = 3; | 最短的合法段是 1 + 2,从它开始。sum 用 long long,换更大的 M 也不用改。 |
while (l < r) | l == r 时窗口只有一个数,不合法,直接结束循环。 |
if (sum == M) | 找到一段,输出首尾两个数。这里不能 break——还有别的段。 |
sum -= l; ++l; | “相等”和“过大”两种分支都要收缩左端,写的动作完全一样。 |
++r; sum += r; | 顺序不能反:先把 r 加 1,再把新的 r 加进 sum;反过来会加进旧的 r,整条和都错位。 |
cout << l << " " << r << "\n"; | 行内一个空格、行末只换行。 |
while (l <= r):M = 10 时会多输出一行 10 10(单数段)→ WA。++r; sum += r; 写成 sum += r; ++r;:加进去的是旧的 r,和永远对不上。