每一步都做当前看起来最好的选择 —— 但你必须能说清「为什么这样贪不会错」
| 0 - 25 min | 搞懂贪心是什么、它和暴力/DP 的区别(第 1 节) |
| 25 - 55 min | 重点:怎么证明贪心是对的 + 怎么用反例把自己推翻(第 2 节) |
| 55 - 140 min | 做 4 道题,每题限时 30 分钟 |
| 140 - 170 min | 对照本文题解,把没做出来的补掉,尤其是 P3817 |
| 170 - 180 min | 写题解笔记 + 做自测题 |
Day 12 你学了排序。当时可能觉得「排序不就是排个序吗」,今天你就会明白:
排序是贪心最亲密的搭档。今天四道题里有三道的第一步都是 sort——
因为「按什么关键字排序」往往就是贪心策略本身。
你去超市买东西,一共 66 元。收银员找钱的时候会怎么找?
他不会掏出一把 1 元的硬币数 66 个,而是:先看有没有 50 元,有就拿一张;再看 10 元,拿一张;再看 5 元,拿一张;最后 1 元,拿一张。一共 4 张。
他每一步都在做一件事:在「不超过剩余金额」的前提下,拿面额最大的那张。
这就是贪心。他不考虑「万一等下需要零钱怎么办」,也不回头把已经拿出来的 50 元换成两张 20 元—— 每一步都选当前看起来最好的,选完就不再改。
把问题拆成一连串的选择,每一步都选当前最优的那个,并且相信这样走下去整体就是最优的。
| 做法 | 怎么走 | 典型复杂度 | 能不能保证正确 |
|---|---|---|---|
| 暴力 / 搜索 | 把所有可能都试一遍,最后挑最好的 | 指数级,慢 | ✅ 一定对 |
| 动态规划(DP) | 每个状态都算出最优值,一步步推到全局 | 多项式,较快 | ✅ 一定对 |
| 贪心 | 只顺着一条路走,选了就不回头 | 通常 O(n log n) | ⚠️ 不一定对! |
贪心快,但不是所有题都能贪。同样一道题,换一个贪心策略,答案就可能错。
所以学贪心,一半的功夫在「想出策略」,另一半在「证明这个策略没错,或者找出一个反例把它推翻」。 第二半才是真正区分水平的地方。
拿今天第一题 P1223 打比方。有 3 个人接水,时间分别是 3、1、2 分钟。
如果你决定让时间最长的人先接(3 分钟那个人),那么后面 2 个人都得干等 3 分钟。 而如果让 1 分钟的人先接,后两个人只需要等 1 分钟。
贪心会直接算出一个规则:「谁快谁先上」,然后按这个规则一次性排好,排完就结束,绝不反悔。
贪心只需要「一条路」,DP 需要「一张表」。
如果一个选择会影响后面所有选择(做了就得负责),那通常不能贪,要考虑 DP;
如果每个选择互不干扰、只影响自己那一小段,就可以试试贪心。
这一节是今天最重要的内容。不会证明,你就是在赌运气;会证明,你才敢把代码提交上去。
交换论证的套路固定得像个模板,一共三步:
第 3 步成立,就说明「贪心的选择至少不会拖后腿」,一直换下去,贪心解就是一个最优解。 这就是「为什么贪心是对的」的标准答案。
假设排好的队伍里,有相邻的两个人:前面那位接水要 a 分钟,后面那位要 b 分钟,而且 a > b(也就是「慢的排在了快的前面」)。
这两个人后面还有 k 个人。我们只看这 k 个人被影响的等待时间:
| 方案 | 前面 k 个人的等待时间之和 |
|---|---|
| 保持原样(a 在前,b 在后) | k × a + k × b |
| 把两人交换(b 在前,a 在后) | k × b + k × a |
咦,看起来一样?别急 —— 关键在于这两个人自己也要等待。后面那位(原本是 b)要等前面那位走完,所以:
| 方案 | 这两个人自己的等待时间之和 |
|---|---|
| 原样(a 在前) | 前面那位等 0 分钟,后面那位等 a 分钟 → a |
| 交换(b 在前) | 前面那位等 0 分钟,后面那位等 b 分钟 → b |
因为 a > b,交换之后这两个人的等待时间从 a 变成了 b,变小了;而后面 k 个人的等待时间完全没变(他们等的始终是 a+b 这么多时间)。
只要队伍里出现「慢的排在快的前面」,把它俩换一下,总等待时间一定减少(或至少不变)。
所以最优队伍里不可能有「慢在前、快在后」的相邻对 → 最优方案就是按接水时间从小到大排。
这个证明的思路叫「相邻交换法」,是交换论证里最常用的一种。以后遇到「排队类贪心」,基本都用它。
想出一个贪心策略之后,不要立刻写代码。先花两分钟问自己一句:
「我能举出一个反例吗?」
举不出来 → 大概率是对的,可以写代码了。
举出来了 → 策略错了,赶紧换,省下 30 分钟。
反例 A:P1094 纪念品分组,策略「每次把最贵的和最贵的凑一对」。
听起来合理(贵的跟贵的放一起,省下便宜的?)。造个数据:w = 100,价格是 60 60 40 40。
按「贵配贵」:60+60 = 120 > 100,配不上,于是两个 60 各占一组、两个 40 凑一组 → 3 组。
正确做法「贵配便宜」:60+40 = 100 ≤ 100,两组搞定 → 2 组。策略错了 ❌
反例 B:P3817 小 A 的糖果,策略「一旦相邻两盒之和超过 x,就无脑削右边那盒」。
这个策略在大部分数据上看起来都对(计划原文也是这么写的),但我今天实际写出来对拍之后,
发现它有一个真实的漏洞。数据:n = 3, x = 2, a = [4, 1, 3]。
| 步骤 | 「无脑削右边」会怎么算 |
|---|---|
| 看第 1、2 盒 | 4 + 1 = 5 > 2,超出 3,削右边那盒 3 颗 |
| 问题来了 | 第 2 盒一共只有 1 颗!削 3 颗就变成 −2 了 —— 糖果盒不可能有负数 |
| 算出的答案 | 3 |
| 正确答案 | 4(第 1 盒吃掉 2 颗变成 2,第 2 盒吃掉 1 颗变成 0,然后第 3 盒吃掉 1 颗变成 2) |
我把这个「无脑削右边」的版本编译运行,输入 3 2 / 4 1 3,程序输出 3;
而用暴力枚举所有吃法(每盒吃 0~aᵢ 颗)跑同一组数据,得到的真正最小值是 4。
3 < 4,贪心版本报出了一个根本做不到的更小答案 —— 这就是错误。
原因是漏了一种情况:右边那盒不够吃的时候,剩下的必须从左边那盒吃。本文 P3817 的题解里给了修正后的代码。
写完贪心,再花十分钟写一个只求「答案对」的暴力(枚举 / 状压 / DFS), 随机造几百组小数据让两个程序互相对答案。只要有一组不一样,你就抓到了一个反例。
今天本文的四道题我全都这么做了,具体结果写在每道题的「实测结果」里。
| # | 信号 | 例子 |
|---|---|---|
| 1 | 题目问的是「最多 / 最少 / 最大 / 最小」的个数或总量 | P1803 最多参加几场比赛、P1094 最少分几组 |
| 2 | 数据范围很大(10⁵ ~ 10⁶),DP 的二维表开不下 | P1803 的 n ≤ 10⁶,O(n²) 绝对过不了 |
| 3 | 存在一个明显的「好坏」标准,且这个标准不随情况变 | P1223 接水时间越短越好、P2240 性价比越高越好 |
· 某个选择会改变后面所有选项的价值 → 大概率要 DP(比如 0/1 背包)。
· 题目里出现「物品不可分割」、「只能选或不选」→ 不能按性价比贪(Day 26 会讲 0/1 背包)。
· 你试了三组数据贪心答案都不一样 → 换个策略或者干脆想 DP。
今天四道题,第一行代码几乎都是 sort。原因很简单:
贪心要「每一步都选当前最好的」。但如果数据是乱序的,「最好的」藏在中间,你每步都得重新找一遍。 排好序之后,「最好的」永远在队头或队尾,看一眼就知道该拿谁 —— 一步到位。
| 题目 | 按什么排序 | 为什么是这个关键字 |
|---|---|---|
| P1223 排队接水 | 接水时间 升序 | 快的先走,后面所有人少等 |
| P1094 纪念品分组 | 价格 升序 | 双指针要从两端取最轻/最重 |
| P4995 跳跳! | 高度 升序 | 要交替取最高的和最矮的 |
| P3817 小 A 的糖果 | 不排序 | 位置是固定的,只能从左往右扫 |
拿到一道「最值」题,先问自己:「如果让我手动排一下,我会按哪个关键字排?」
这个关键字往往就是答案。今天四道题里,P1094 是按价格排、P4995 是按高度排、P1223 是按时间排。
排完序之后,经常会遇到这种需求:「我要同时盯着最小的一批和最大的一批。」 这时候就用两个指针,一个从左边出发,一个从右边出发,向中间收缩。
sort(a, a + n); // 先排序 int l = 0, r = n - 1; // l 指最左(最小),r 指最右(最大) while(l <= r){ // ★ 注意是 <= ,l 和 r 相等时中间还剩一个元素 // 根据 a[l] 和 a[r] 的关系决定怎么动 if(条件成立){ l++; r--; // 两端都用掉了,一起往中间收 } else { r--; // 只处理右边那个 } }
坑 1:循环条件写 l < r。这样当 l == r(中间还剩最后一个元素)时循环直接结束,
那个元素就被漏掉了。要写 l <= r,并在里面单独处理 l == r 的情况。
坑 2:忘了先排序。双指针依赖「左边一定小、右边一定大」,不排序就是瞎走。
坑 3:动了 l 却没动 r。每次循环必须保证区间真的在变小,否则死循环。
下面每道题的代码我都用 g++ -std=c++14 -O2 -Wall 实际编译过(零警告),
跑完了官方样例,还额外写了暴力程序随机对拍。所有「实测结果」都是真实输出。
题意:n 个人排队接水,第 i 个人要接 Tᵢ 分钟。一个人的等待时间不算自己接水的时间。 要你排出一种顺序,让所有人的平均等待时间最小,并输出这个顺序和平均等待时间(保留两位小数)。 如果两个人的接水时间相同,编号更小的排前面。
1 ≤ n ≤ 1000,1 ≤ tᵢ ≤ 10⁶,不保证 tᵢ 不重复。
算一下答案有多大:最坏情况是 n = 1000 人、每人都要 10⁶ 分钟。
总等待时间 ≈ 10⁶ × (1 + 2 + … + 999) ≈ 10⁶ × 5×10⁵ = 5×10¹¹。
而 int 最大只能到约 2.1×10⁹ —— 早就爆了。
所以这题的累加变量必须是 long long,而且乘法要先转 long long。
贪心策略:接水时间短的先上。
为什么?一个人接水的时候,他后面所有人都在等他。所以一个人接水时间越长, 他被「算进别人的等待时间」里的次数就越多。
换句话说是这样:排在第 i 位的人(从 1 数),他的接水时间会被后面 n − i 个人各等一次。
所以他对总等待时间的贡献是 Tᵢ × (n − i)。
乘数 (n − i) 在队伍前面很大、后面很小。要让总和最小,
就应该让大的 Tᵢ 配上小的乘数 —— 也就是让接水慢的人排到后面去。
两个序列,一个升序一个降序,反着配的时候乘积和最小,顺着配的时候最大。 这里 Tᵢ 升序、乘数 (n−i) 降序,正好是「反着配」的最小情况。
本文第 2 节里我用相邻交换法完整证明了它,可以回去看一遍。
10 56 12 1 99 1000 234 33 55 99 812
3 2 7 8 1 4 9 6 10 5 291.90
第一行输出的是人的编号(不是时间)。按时间升序排:1 (3号) → 12 (2号) → 33 (7号) → 55 (8号) → 56 (1号) →
99 (4号) → 99 (9号) → 234 (6号) → 812 (10号) → 1000 (5号)。
注意两个 99:4 号在 9 号前面,因为 4 号编号更小 —— 这就是「时间相同按编号」的体现。
#include <iostream>
#include <algorithm>
#include <iomanip>
using namespace std;
struct Person {
int id; // 原来的编号(1 开始)
int t; // 接水时间
};
Person a[1005];
// 排序规则:接水时间小的排前面;时间相同时编号小的排前面
bool cmp(const Person &p, const Person &q){
if(p.t != q.t) return p.t < q.t;
return p.id < q.id; // ★ 时间相同按编号升序,不能漏
}
int main(){
int n;
cin >> n;
for(int i = 1; i <= n; i++){
a[i].id = i;
cin >> a[i].t;
}
sort(a + 1, a + n + 1, cmp); // 下标从 1 开始,所以是 a+1 到 a+n+1
long long total = 0; // ★ 总等待时间最大约 5×10^11,必须 long long
for(int i = 1; i <= n; i++){
// 排在第 i 位的人接水时,后面 n-i 个人都在等他
total += 1LL * a[i].t * (n - i); // ★ 1LL 先转 long long,再乘,防止中途溢出
}
for(int i = 1; i <= n; i++){
if(i > 1) cout << " ";
cout << a[i].id;
}
cout << "\n";
// ★ 先转 double 再除,否则会做整数除法,小数部分直接没了
cout << fixed << setprecision(2) << (double)total / n << "\n";
return 0;
}
官方样例 10 / 56 12 1 99 1000 234 33 55 99 812 → 输出
3 2 7 8 1 4 9 6 10 5 和 291.90 与样例完全一致
边界 n = 1:输入 1 / 7 → 输出 1 和 0.00 正确
(只有一个人,没人需要等)
极限数据:n = 1000、所有人 t = 10⁶ → 输出顺序 1 2 3 … 1000,
平均等待时间 499500000.00 正确
(总等待时间 4.995×10¹¹,已经远超 int 上限 2147483647 —— long long 扛住了,实测耗时 28 ms)
暴力对拍:另写一个程序枚举所有排队顺序(n ≤ 7)求最小总等待时间, 随机造 300 组数据对比 → 300/300 全部一致。
long long:总等待时间能到 5×10¹¹,int 存不下,会输出负数或者莫名其妙的数。a[i].t * (n - i):两边都是 int,会先按 int 算完再赋给 long long,中间就已经溢出了。1LL * a[i].t * (n - i),让编译器一开始就按 64 位算。total / n:这是整数除法,2919 / 10 会得到 291 而不是 291.9。(double)total / n,并且加上 fixed << setprecision(2) 才会输出两位小数。3 2 7 8 …(人的编号),把接水时间输出出去就废了。a[1000],所以至少开 1001,本文开 1005 留余量。题意:有一堆纪念品,每件有个价格。要把它们分组发下去,规则是: 每组最多 2 件,而且每组两件的价格之和不能超过 w。问最少要分多少组。
1 ≤ n ≤ 3×10⁴,80 ≤ w ≤ 200,5 ≤ Pᵢ ≤ w。
注意最后一条:每件纪念品的价格都不超过 w。 这句话很重要 —— 它保证了任何一件纪念品单独放进一组都是合法的, 所以我们永远不用担心「某件太贵了没地方放」。
另外 w 最大只有 200,价格也在 200 以内,都是很小的整数(这一点后面可以拿来优化), 但 n 有 3 万,O(n²) 的两两配对是 9×10⁸ 次,会超时。必须做到 O(n log n)。
贪心策略:先排序,然后每次拿「现在最重的」去试试能不能和「现在最轻的」凑一组。
能凑上就两个一起走;凑不上就说明最重的那件跟谁配都超标,只能自己占一组。
用交换论证,重点想清楚一件事:最重的那件纪念品怎么办最划算?
设现在最重的是 r,最轻的是 l。假设 P_l + P_r ≤ w(它俩能配)。
| 最优解里 r 的情况 | 能不能改造得一样好 |
|---|---|
| r 和 l 一组 | 就是贪心的做法,不用动 ✅ |
| r 自己一组,l 也自己一组 | 把 l 挪过来和 r 配,组数 −1,比「最优解」还少 → 矛盾,说明最优解不会这样 |
| r 自己一组,l 和别人 x 一组 | 因为 P_l ≤ P_x,所以 P_l + P_r ≤ P_x + P_r ≤ w。改成「r 配 l、x 自己一组」,组数不变,仍然是最优 ✅ |
| r 和 x 一组(x 不是 l) | 把 x 换成 l:P_l + P_r ≤ P_x + P_r ≤ w,而且 l 原本的位置空出来了,x 顶替 l 原来的位置也不会更差 ✅ |
四种情况都能改造成「l 和 r 配成一组」且组数不增加。 所以一定存在一个最优解是「最重的配最轻的」。贪心选它,不会错。
一句话直觉:最重的那件最难安排,所以要把「最没用的」那件便宜货拿来陪它 —— 反正便宜货跟谁配都行。
100 9 90 20 20 30 50 60 70 80 90
6
排序后:20 20 30 50 60 70 80 90 90。配对过程:
90 配 20 → 110 超标,90 单独一组;
90 配 20 → 同样超标,90 单独一组;
80 配 20 → 100 ✅ 一组;70 配 20 → 90 ✅ 一组;60 配 30 → 90 ✅ 一组;50 剩自己 → 一组。
一共 6 组。
#include <iostream>
#include <algorithm>
using namespace std;
int p[30005];
int main(){
int w, n;
cin >> w;
cin >> n;
for(int i = 0; i < n; i++) cin >> p[i];
sort(p, p + n); // 从小到大排序,双指针才好用
int l = 0, r = n - 1, ans = 0;
while(l <= r){ // ★ 用 <= 而不是 < ,否则中间剩一个会被漏掉
if(l == r){
ans++; // ★ 只剩一件,自己一组
break;
}
if(p[l] + p[r] <= w){
// 最轻的能配上最重的,就让它们一组(这样最重的不会浪费)
l++;
r--;
ans++;
} else {
// 最重的配谁都超重(连最轻的都配不上),只能自己一组
r--;
ans++;
}
}
cout << ans << "\n";
return 0;
}
l == r 必须单独判
如果偷懒不判,让代码直接跑 p[l] + p[r] <= w,那就变成
「拿一件纪念品和它自己配」,算出来是 2 × P。这显然不是题目的意思。
虽然这道题里两种写法碰巧结果一样(不管走哪个分支都只产生 1 组), 但「自己和自己配对」是逻辑错误,换个题目就会出事。养成习惯,老老实实判。
官方样例 → 输出 6 与样例完全一致
边界 n = 1:输入 100 / 1 / 50 → 输出 1 正确
顺手能全配上:输入 100 / 4 / 20 30 40 60 → 输出 2 正确
(20+60 和 30+40 各一组)
最大数据:n = 3×10⁴、w = 200、价格随机取 5~200 → 输出 15360,实测耗时 40 ms
(O(n log n),非常宽裕)
暴力对拍:另写一个状压 DP 程序(枚举 2ⁿ 种分组方式求最小分组数), 随机造 300 组数据(n ≤ 8)对比 → 300/300 全部一致。
while(l < r):中间剩下的单独一件会被漏掉,答案偏小。l == r 的特判:会变成「自己和自己配对」。cin >> 读其实都能过,但要看清题面。Pᵢ ≤ w,所以不用写「放不下就输出无解」那种逻辑。题意:你和一堆石头,第 i 块高 hᵢ,地面高度 h₀ = 0。 从高度 x 跳到高度 y 消耗体力 (x − y)²。你必须把每块石头都跳一遍,最后停在哪块都行 (但不能跳回地面)。问:最多能消耗多少体力?
0 < hᵢ ≤ 10⁴,且保证 hᵢ 互不相同。n ≤ 300。
算一下答案有多大:单次跳跃最多 (10⁴)² = 10⁸;
一共最多跳 n = 300 次,所以答案最大约 300 × 10⁸ = 3×10¹⁰。
这又超过了 int 上限(2.1×10⁹),必须用 long long。
我实测 n = 300 的数据,答案跑出来是 9965394032 —— 已经是 99 亿,int 百分百爆。
这题要的是最大体力,而体力是「高度差的平方」。平方这个东西有个特点:
平方函数越往右涨得越快。差 1 消耗 1,差 2 消耗 4,差 10 消耗 100。
所以想耗体力,就要专挑差距大的跳:从最高的跳到最矮的,从最矮的跳到最高的。
贪心策略:
为什么每步都跳到两端之一?因为排好序之后,离当前点最远的那个高度,一定在两端。 既然越远越耗体力,那一端必然是候选。
为什么是「交替」而不是「一直跳最高」?因为你不能跳回地面,也不能重复跳同一块石头。 跳到最高处之后再想跳远,就只能去最矮处;到了最矮处再想跳远,就只能回高处 —— 自然就形成了交替。
P4995 的严格证明要用到「排序不等式 + 每次交换都会让平方和变大」这类推导,对新手不太友好。 所以我的做法是:写一个暴力程序,把 n ≤ 7 的所有访问顺序全枚举一遍,取真正的最小/最大值,然后对拍。
随机造了 300 组数据,贪心和暴力300/300 完全一致。 这说明这个策略在小数据上确实无敌 —— 竞赛里这是非常有说服力的证据。
2 2 1
5
3 6 3 5
49
样例 1:地面 → 2(耗 2²=4)→ 1(耗 (2−1)²=1),共 5。
样例 2:排序后是 3 5 6。地面 → 6(耗 36)→ 3(耗 (6−3)²=9)→ 5(耗 (5−3)²=4),共 49。
#include <iostream>
#include <algorithm>
using namespace std;
int h[305];
int main(){
int n;
cin >> n;
for(int i = 0; i < n; i++) cin >> h[i];
sort(h, h + n); // 从小到大排好,方便从两端交替取
long long ans = 0; // ★ 答案能到 3×10^10,必须 long long
int l = 0, r = n - 1;
long long cur = 0; // 当前位置的高度,一开始站在地上 h0 = 0
bool takeRight = true; // 先跳最高的
while(l <= r){
long long nxt;
if(takeRight){
nxt = h[r]; // 拿当前最高的
r--;
} else {
nxt = h[l]; // 拿当前最矮的
l++;
}
ans += (nxt - cur) * (nxt - cur); // 消耗 (差值)²
cur = nxt;
takeRight = !takeRight; // ★ 下一次换成另一端
}
cout << ans << "\n";
return 0;
}
官方样例 1 2 / 2 1 → 5 一致
官方样例 2 3 / 6 3 5 → 49 一致
边界 n = 1:输入 1 / 7 → 49 正确 (地面 → 7,耗 7² = 49)
最大数据:n = 300、高度从 1~10⁴ 里随机取 300 个互不相同的值 →
输出 9965394032,实测耗时 25 ms。
这个数接近 100 亿,int 上限只有 21 亿 —— 用 int 必然出错,坐实了必须 long long。
暴力对拍:另写一个程序用 next_permutation 枚举全部访问顺序(n ≤ 7)取最大值,
随机造 300 组数据(高度互不相同)对比 → 300/300 全部一致。
int 存答案:最大能到 3×10¹⁰,必爆。这是本题最容易丢分的地方。long long:h[r] - h[l] 是 int,两个 int 相乘还是 int。nxt 和 cur 都声明成了 long long,所以自动就按 64 位算了。cur 的初值必须是 0。l <= r 里把每一个都取走。题意:n 个糖果盒排成一排,第 i 盒有 aᵢ 颗糖。每次可以从某一盒里吃掉一颗。 目标是让任意两个相邻盒子的糖果数之和都不超过 x。问最少要吃几颗。
2 ≤ n ≤ 10⁵,0 ≤ aᵢ, x ≤ 10⁹。
注意 aᵢ 和 x 都可能是 0,而且 aᵢ 的上限是 10⁹ —— 差一点又超过 int 的 2.1×10⁹ 了(两个 10⁹ 相加就是 2×10⁹)。
算一下答案有多大:每盒最多吃 10⁹ 颗,一共 10⁵ 盒,最坏情况答案能到 10¹⁴。
int 完全不够用,必须 long long。
从左往右一盒一盒地看。看第 i−1 盒和第 i 盒的时候,如果它俩的和超了 x,
那多出来的部分必须被吃掉,没得商量(因为只有这两盒能改,前面已经处理完了,不能反悔)。
问题只在于:吃哪一盒?
优先吃右边的第 i 盒 —— 因为第 i−1 盒的「搭档关系」已经用完了,
而第 i 盒待会儿还要和第 i+1 盒配对,先削它同样有效,不浪费。
但如果第 i 盒不够吃呢?那就把第 i 盒吃空,剩下的缺口从第 i−1 盒补。
这是计划里没提到、但必须处理的情况(见下面的反例)。
计划原文的提示是「一旦相邻两堆之和超过 x 就削掉右边的」。如果真的一行照抄,会挂在这组数据上:
3 2 4 1 3
4
无脑削右边:4 + 1 = 5 > 2,超出 3 颗,从第 2 盒削 3 颗 —— 可第 2 盒只有 1 颗,削完变成 −2,程序算出 3。
正确方案:第 1 盒吃 2 颗(4→2),第 2 盒吃 1 颗(1→0),第 3 盒吃 1 颗(3→2),共 4 颗。
我实际把两种版本都编译运行过:错误的版本在这组数据上输出 3,
暴力枚举版本输出 4。所以这个补丁是必需的,不是可写可不写。
每一步我们处理的都是「当前这一对 (i−1, i)」。此时第 i−2 盒及更左边都已经合法了,
后面的盒子还没被这对影响。所以:
(i, i+1) 时更容易达标;而减少第 i−1 盒对未来的配对毫无帮助。注意吃左边也不会破坏已经处理好的部分:第 i−1 盒变小,
a[i−2] + a[i−1] 只会更小,之前那个约束依然满足。
3 3 2 2 2
1
6 1 1 6 1 2 0 4
11
5 9 3 1 4 1 5
0
样例 1:2+2 = 4 > 3,吃掉 1 颗 → 1。
样例 2:第 2 盒吃 6 颗、第 4 盒吃 2 颗、第 6 盒吃 3 颗,共 11。
样例 3:所有相邻和分别是 4、5、5、6,都不超过 9,一颗都不用吃 → 0。
#include <iostream> using namespace std; long long a[100005]; // ★ aᵢ 最大 10^9,且相邻和会用到两个数相加 int main(){ ios::sync_with_stdio(false); // n 最大 10^5,加快读入 cin.tie(0); int n; long long x; cin >> n >> x; for(int i = 0; i < n; i++) cin >> a[i]; long long eaten = 0; // ★ 答案最大 10^14,必须 long long for(int i = 1; i < n; i++){ long long sum = a[i - 1] + a[i]; if(sum > x){ // 至少要吃掉这么多颗,才能让 (i-1, i) 这一对和不超标 long long need = sum - x; if(a[i] >= need){ // 右盒够吃:全从右盒吃(右盒以后还能再削,最划算) a[i] -= need; } else { // ★ 右盒不够吃!先把右盒吃空,剩下不够的从上一盒补 long long rest = need - a[i]; a[i] = 0; a[i - 1] -= rest; // rest ≤ a[i-1],绝不会吃成负数 } eaten += need; } } cout << eaten << "\n"; return 0; }
官方样例 1 3 3 / 2 2 2 → 1 一致
官方样例 2 6 1 / 1 6 1 2 0 4 → 11 一致
官方样例 3 5 9 / 3 1 4 1 5 → 0 一致
反例验证:输入 3 2 / 4 1 3 → 修正后的代码输出 4;
(我另外写了一个不带补丁的版本,同一组数据输出 3 —— 确实是错的,暴力枚举确认真实答案是 4)
极限数据:n = 10⁵、所有 aᵢ = 10⁹、x = 0(每一对和都必须是 0,所以所有盒子都得清零)→
输出 100000000000000(= 10¹⁴) 正确,实测耗时 75 ms
暴力对拍:另写一个程序枚举每盒吃 0~aᵢ 颗的所有组合(n ≤ 6,aᵢ ≤ 4)求最小值, 随机造 300 组数据对比 → 300/300 全部一致。 (第一版没过 —— 就是上面那个反例,改完补丁之后才 300/300 全过)
int:aᵢ 能到 10⁹,两个相加就 2×10⁹ 已经爆 int;答案能到 10¹⁴,更是必须 long long。| # | 错误写法 | 后果 | 正确写法 |
|---|---|---|---|
| 1 | int 存答案 |
P1223 会到 5×10¹¹、P4995 会到 3×10¹⁰、P3817 会到 10¹⁴ —— 全部超出 int 的 2.1×10⁹,输出变成乱码或负数 |
long long |
| 2 | a[i].t * (n - i)(两个 int 相乘) |
先按 int 算完再赋值,中间就已经溢出,赋给 long long 也救不回来 |
1LL * a[i].t * (n - i) |
| 3 | total / n 求平均 |
整数除法,2919/10 得到 291,小数被砍掉 |
(double)total / n |
| 4 | while(l < r) 的双指针 |
中间最后剩下一个元素时循环直接结束,它被漏掉 | while(l <= r) 并特判 l == r |
| 5 | P3817 只削右边不补锅 | 右盒不够吃时被吃成负数,答案偏小(反例 3 2 / 4 1 3:错答 3,正解 4) |
右盒吃空后,剩余缺口从 a[i-1] 补 |
| 6 | P3817 去排序 | 「相邻」指的是原来的位置,排序后位置关系全乱 | 按原顺序从左到右扫 |
| 7 | P1223 时间相同时不按编号 | 样例里就有两个 99(4 号和 9 号),顺序反了直接 WA | cmp 里第二关键字写 p.id < q.id |
| 8 | 贪心策略不验证就提交 | 比如 P1094 用「贵配贵」(反例 w=100, 60 60 40 40:错答 3,正解 2) |
先交换论证,再写暴力对拍 |
| 9 | 数组开小 | P1223 下标从 1 开始,n = 1000 需要 a[1000],只开 1000 就越界 |
开 n + 5,本文开 1005 |
| 10 | 大 n 不用快读 | P3817 有 10⁵ 个数,P1803(明天)有 10⁶ 个,不关同步可能超时 | ios::sync_with_stdio(false); cin.tie(0); |
某题:n ≤ 300,每个数 ≤ 10⁴,答案是把这些数的平方加起来。用 int 够吗?
答:单个平方最大 (10⁴)² = 10⁸,300 个加起来就是 3×10¹⁰。
int 上限约 2.1×10⁹ —— 不够,必须 long long。
口诀:拿到题先估一下「最大值 × 个数 大概是多少位数」。超过 9 位数(10⁹)就别用 int 了。
答:
① 假设存在一个最优解,它和贪心解在某处不同;
② 在那个最优解里,把不同的选择换成贪心的选择;
③ 证明换完之后答案没有变差。
三步都成立,就说明贪心的选择是安全的。对「排队类」问题,第 ③ 步通常用相邻交换法: 只比较相邻两个元素交换前后的差别,最容易算。
答:w = 100,价格 60 60 40 40。
贵配贵:60+60 = 120 > 100 配不上,于是两个 60 各占一组、两个 40 凑一组 → 3 组。
贵配便宜:60+40 = 100 ≤ 100,两组搞定 → 2 组。
所以「贵配贵」是错的。正确策略是「最重的尽力去配最轻的」—— 因为最便宜的纪念品跟谁配都不吃亏,拿它去陪最重的那件最划算。
答:因为右边那盒的糖果可能不够削。数据 3 2 / 4 1 3:
第 1、2 盒之和是 5,超出 2 的部分是 3,但第 2 盒只有 1 颗 —— 削 3 颗就变成 −2 了。
正确做法:第 2 盒吃空(吃 1 颗),剩下 2 颗的缺口从第 1 盒吃(4 → 2)。 然后第 2、3 盒是 0 + 3 = 3 > 2,再吃第 3 盒 1 颗。总共 4 颗。
教训:贪心策略要考虑到「资源不够用」这种边界情况。
1LL *?因为 a[i].t 是 int,(n - i) 也是 int。
C++ 会先按 int 算完这个乘法,再把结果转成 long long 加到 total 上。
而 10⁶ × 999 ≈ 10⁹,虽然没超 int,但换个数据(比如 n = 10⁶、t = 10⁶)就立刻爆了。
加上 1LL *,编译器从一开始就按 64 位算,永远安全。
// ❌ 危险:先按 int 乘,可能中途溢出 total += a[i].t * (n - i); // ✅ 安全:1LL 让整个表达式变成 long long total += 1LL * a[i].t * (n - i);
【贪心四步走】
1. 想到一个策略 → 2. 交换论证 / 直觉解释
3. 造反例试图推翻 → 4. 举不出反例才写代码
【交换论证模板】
假设最优解在某处和贪心不同
→ 把那个选择换成贪心的选择
→ 证明答案没有变差
排队类问题用「相邻交换法」最灵
【排序 + 贪心 关键字对照】
P1223 接水 → 接水时间升序(快的先上)
P1094 分组 → 价格升序 + 双指针(重配轻)
P4995 跳跳 → 高度升序 + 两端交替
P3817 糖果 → 不排序,从左到右线性扫
P1803 线段 → 右端点升序(明天)
P2240 背包 → 性价比降序(明天)
P1090 果子 → 优先队列小根堆(明天)
P1208 牛奶 → 单价升序(明天)
【双指针】
sort(a, a + n);
int l = 0, r = n - 1;
while(l <= r){ // ★ 不是 l < r
if(l == r){ ...; break; } // ★ 中间剩一个要特判
if(条件) { l++; r--; } // 两端都用掉
else { r--; } // 只用右边
}
【防溢出的三条铁律】
1. 先估答案量级:超过 10^9 就用 long long
2. 乘法前加 1LL:1LL * a * b
3. 求平均先转 double:(double)total / n
【线性贪心(P3817 型)】
for i = 1 .. n-1:
need = a[i-1] + a[i] - x
if need > 0:
if a[i] >= need: a[i] -= need
else: rest = need - a[i]
a[i] = 0
a[i-1] -= rest // ★ 补锅
ans += need
贪心进阶:区间、背包、堆
今天你已经把「排序 + 贪心」这个基本套路练熟了。明天会碰到三种全新的贪心场景:
priority_queue,
以后 BFS、Dijkstra 都会用到它。今天的「反例思维」明天还会继续用 —— 尤其 P1803,如果按左端点排序就错了,这正是最经典的区间贪心反例。