NOI C++ 课程体系 / 算法篇 / 第3讲
CSP-J 入门级 · 基础算法 · m4-03

贪心法

每一步都选眼前最好的 —— 掌握贪心策略的设计思想与经典应用,学会判断贪心的正确性,在竞赛中快速拿分!

6 大模块 2 大适用条件 3 个经典案例 5 大易错点 12 道分层练习
本讲 · 第 27/56 讲贪心法
模块 01

情景导航

用生活中的决策场景,直觉地理解"贪心"思想。

故事找零钱的贪心策略

小明去商店买东西,付了100元,找零 67 元。收银员要怎么给零钱呢?

收银员脑子里的"算法"是这样的:每次都尽量给面值最大的钞票/硬币。

第1步:67 ≥ 50 → 给一张 50元,剩余 17
第2步:17 ≥ 10 → 给一张 10元,剩余 7
第3步:7 ≥ 5 → 给一张 5元,剩余 2
第4步:2 ≥ 1 → 给两枚 1元,剩余 0 ✅

总共 5 张,这就是最优解!

这种"每次选当前最好的"的策略,就是贪心法。收银员不需要考虑所有组合方式,只要每步都贪心地选最大面值,就能得到最少的钞票数。

贪心 = 每一步都做局部最优选择 → 期望达到全局最优

💡 但贪心不一定总是对的!

如果硬币面值改成 1、3、4,要找零 6元:

• 贪心:4+1+1 = 3 枚

• 最优:3+3 = 2 枚 ✅

贪心失败了!因为"每次选最大"这个策略在这组面值下行不通。

关键问题:什么时候贪心是对的?什么时候会出错?

①生活类比:贪心无处不在
💰

找零钱

每次给面值最大的钞票,用最少张数找零。

→ 贪心选最大
📺

看电视节目

尽量多看完整节目 → 优先选结束最早的。

→ 活动选择问题
🎒

装背包

背包容量有限,优先装"性价比"最高的物品。

→ 按单位价值排序
🚗

排队加油

让用时短的车先加油,总等待时间最少。

→ 短作业优先
②编程定义

贪心法(Greedy Algorithm):在对问题求解时,总是做出当前看来最好的选择。也就是说,不从整体最优上加以考虑,所做出的仅是某种意义上的局部最优解。

如果问题具有贪心选择性质和最优子结构,那么贪心法就能保证得到全局最优解。

📌 本讲核心知识地图

板块核心内容对应大纲
贪心思想局部最优 → 全局最优贪心法
适用条件贪心选择性质 + 最优子结构贪心正确性分析
活动选择按结束时间排序,贪心选最早结束的经典贪心案例
分数背包按单位价值排序,贪心选性价比最高的经典贪心案例
合并果子贪心选最小的两堆合并(Huffman思想)经典贪心案例
③核心口诀
🔑 贪心法核心

每步贪心选最好,排序之后再挑选;
局部最优望全局,证明正确才算妙!
贪心不对换 DP,暴力兜底不丢分!

🔑 贪心三步骤

① 排序:按某个关键字排序(结束时间、性价比…)
② 挑选:依次扫描,满足条件就选
③ 更新:更新当前状态(剩余容量、最后结束时间…)

模块 02

知识探究

深入理解贪心法的适用条件、设计步骤,并通过三个经典案例掌握实战技巧。

1贪心法的两大适用条件 核心

并非所有问题都能用贪心法解决。一个问题是贪心可解的,必须同时满足以下两个条件:

🎯 贪心选择性质
  • 定义:全局最优解可以通过一系列局部最优选择得到
  • 每步只需考虑当前最好的选择,不需要回溯
  • 这是贪心法与动态规划的本质区别
  • 例:找零钱(1/5/10/50面值)→ 每次选最大面值就是最优
🧩 最优子结构
  • 定义:问题的最优解包含子问题的最优解
  • 大问题的最优解可以由小问题的最优解组合而成
  • 例:活动选择中,去掉已选活动后的剩余问题仍是最优
  • 这是贪心法和动态规划的共同前提
比较贪心法动态规划
选择方式每步做当前最优选择,不回头考虑所有子问题,自底向上合并
是否需要贪心性质✅ 必须有贪心选择性质❌ 不需要
是否需要最优子结构✅ 必须有✅ 必须有
效率通常 O(n log n)(排序)通常 O(n²) 或更高
正确性需要严格证明状态转移方程保证正确
2贪心法设计四步骤
1
🤔
确定策略

找出"每步该怎么选"——按什么排序?选什么样的?

2
📊
排序

按贪心关键字对所有元素排序

3
✅
逐一挑选

依次扫描,满足约束就选入,更新状态

4
🔍
验证正确性

用小样例检验,或严格证明贪心策略正确

🔑 常见贪心策略

• 最早结束优先(活动选择、区间调度)
• 最短作业优先(排队等待、任务调度)
• 最大性价比优先(分数背包)
• 最小代价优先(合并果子、Huffman编码)
• 字典序最小(字符串贪心构造)

案例经典案例1:活动选择问题 高频

有 n 个活动,每个活动有开始时间和结束时间 [sᵢ, eᵢ)。同一时间只能参加一个活动。问最多能参加几个活动?

贪心策略:按结束时间从早到晚排序,依次选择结束最早且不与已选活动冲突的活动。

📊 活动安排可视化(6个活动)
A1
[1,4)
A2
[3,8)
A3
[0,5)
A4
[5,8)✓
A5
[6,10)
A6
[8,11)✓
时间轴 → 0      2      4      6      8      10     12

排序后(按结束时间):A1[1,4) → A3[0,5) → A2[3,8) → A4[5,8) → A5[6,10) → A6[8,11)

贪心选择过程:

① 选 A1[1,4) — 结束最早,选!当前结束时间 = 4
② A3[0,5) — 开始0 < 4,冲突,跳过
③ A2[3,8) — 开始3 < 4,冲突,跳过
④ A4[5,8) — 开始5 ≥ 4,选!当前结束时间 = 8
⑤ A5[6,10) — 开始6 < 8,冲突,跳过
⑥ A6[8,11) — 开始8 ≥ 8,选!当前结束时间 = 11

✅ 最多参加 3 个活动:A1、A4、A6

C++activity_selection.cpp
#include <iostream>
#include <algorithm>
using namespace std;

struct Activity {
    int s, e;
};

// 按结束时间升序排序
bool cmp(Activity a, Activity b) {
    return a.e < b.e;
}

int main() {
    int n; cin >> n;
    Activity a[1005];
    for (int i = 0; i < n; i++)
        cin >> a[i].s >> a[i].e;

sort(a, a + n, cmp);  // 按结束时间排序

int cnt = 0, last_end = 0;
    for (int i = 0; i < n; i++) {
        if (a[i].s >= last_end) {  // 不冲突
            cnt++;
            last_end = a[i].e;    // 更新结束时间
        }
    }
    cout << cnt << endl;
    return 0;
}
运行结果
输入:6
1 4  3 8  0 5  5 8  6 10  8 11
输出:3

💡 为什么"按结束时间排序"是对的?

交换论证:假设最优解中第一个活动不是结束最早的A,而是活动B(B.e > A.e)。

由于 A.e < B.e,把B替换为A后,不会与后面的活动冲突(A更早结束,给后面留了更多时间),解不会变差。

所以"选结束最早的活动"这个贪心选择一定在某个最优解中。✓

案例经典案例2:分数背包 经典

背包容量为 W,有 n 个物品,每个物品有重量 wᵢ 和价值 vᵢ。物品可以分割(取一部分装入),问背包能装的最大价值是多少?

贪心策略:按"单位价值"(vᵢ/wᵢ)从高到低排序,优先装性价比最高的物品。

📖 示例

背包容量 W=50,三个物品:

物品重量 w价值 v单位价值 v/w
A10606.0 ⭐
B201005.0
C301204.0

贪心选择:

① 装A(全装):重量 10,价值 60,剩余容量 40

② 装B(全装):重量 20,价值 100,剩余容量 20

③ 装C(部分):只能装 20/30 = 2/3,价值 120 × 2/3 = 80

总价值 = 60 + 100 + 80 = 240 ✅

C++fractional_knapsack.cpp
#include <iostream>
#include <algorithm>
using namespace std;

struct Item {
    double w, v, ratio;  // 重量、价值、单位价值
};

bool cmp(Item a, Item b) {
    return a.ratio > b.ratio;  // 按单位价值降序
}

int main() {
    int n; double W;
    cin >> n >> W;
    Item items[1005];
    for (int i = 0; i < n; i++) {
        cin >> items[i].w >> items[i].v;
        items[i].ratio = items[i].v / items[i].w;
    }

sort(items, items + n, cmp);

double total_val = 0;
    for (int i = 0; i < n; i++) {
        if (W >= items[i].w) {
            // 全装
            total_val += items[i].v;
            W -= items[i].w;
        } else {
            // 装一部分
            total_val += items[i].ratio * W;
            W = 0;
            break;
        }
    }
    cout << total_val << endl;
    return 0;
}
运行结果
输入:3 50
10 60
20 100
30 120
输出:240
案例经典案例3:合并果子 NOIP真题

有 n 堆果子,每堆有 aᵢ 个。每次可以合并任意两堆,代价为两堆果子数之和。问将所有果子合并成一堆的最小总代价。

贪心策略:每次选当前最小的两堆合并。(这就是 Huffman 编码的思想!)

📖 示例:4 堆果子 [1, 2, 9, 3]

① 最小两堆:1+2=3,代价=3,新堆:[3, 9, 3] → 排序 [3, 3, 9]

② 最小两堆:3+3=6,代价=6,新堆:[6, 9]

③ 最后合并:6+9=15,代价=15

总代价 = 3 + 6 + 15 = 24 ✅

C++merge_fruits.cpp — 用优先队列(小根堆)
#include <iostream>
#include <queue>
using namespace std;

int main() {
    int n; cin >> n;
    priority_queue<int, vector<int>, greater<int>> pq;  // 小根堆
    for (int i = 0; i < n; i++) {
        int x; cin >> x;
        pq.push(x);
    }

long long total_cost = 0;
    while (pq.size() > 1) {
        int a = pq.top(); pq.pop();  // 最小
        int b = pq.top(); pq.pop();  // 次小
        int cost = a + b;
        total_cost += cost;
        pq.push(cost);                // 合并后放回去
    }
    cout << total_cost << endl;
    return 0;
}
运行结果
输入:4
1 2 9 3
输出:24

💡 为什么用优先队列(小根堆)?

每次需要取出最小的两个元素,合并后再放回去。

如果用排序+数组,每次插入需要 O(n),总复杂度 O(n²)。

用优先队列(小根堆),每次取最小和插入都是 O(log n),总复杂度 O(n log n)。

priority_queue<int, vector<int>, greater<int>> — 记住这个写法!

⚠️贪心失效的场景 重要

贪心法并非万能。以下场景贪心得不到最优解,需要动态规划或其他方法:

❌ 0/1 背包问题
  • 物品不可分割,要么全拿要么不拿
  • 贪心按性价比排序后可能装不满背包
  • 例:容量50,物品[10,60][20,100][30,120]
  • 贪心选 A+B=160,但 A+C=180 更优
  • → 必须用动态规划
❌ 非标准面值的找零
  • 面值 1, 3, 4,找零 6
  • 贪心:4+1+1 = 3 枚
  • 最优:3+3 = 2 枚
  • → 必须用动态规划
问题贪心是否最优正确方法
分数背包(物品可分割)✅ 是贪心(按单位价值排序)
0/1 背包(物品不可分割)❌ 否动态规划
活动选择✅ 是贪心(按结束时间排序)
加权活动选择(有权值)❌ 否动态规划
最短路径(非负权)✅ 是(Dijkstra)贪心 + 优先队列
最短路径(有负权)❌ 否Bellman-Ford
最小生成树✅ 是(Kruskal/Prim)贪心
🔑 判断贪心正确性的方法

① 举反例:如果能找到反例 → 贪心不对
② 交换论证:假设最优解与贪心解不同,交换后不会变差 → 贪心正确
③ 数学归纳法:假设对 n-1 规模成立,推导 n 规模也成立
④ 竞赛技巧:对拍(贪心 vs 暴力/DP),大量随机数据验证

模块 03

实践应用

通过分析和编程题,巩固贪心策略的设计与正确性判断能力。

题1贪心策略选择

题目:n 个人排队打水,第 i 个人打水需要 tᵢ 分钟。只有一个水龙头,如何安排顺序使所有人等待时间之和最小?

A. 按打水时间从长到短排序
B. 按打水时间从短到长排序
C. 按到达先后排序
D. 随机排列

查看参考答案

答案:B. 按打水时间从短到长排序(短作业优先 SJF)

分析:第 i 个人的等待时间 = 前面所有人打水时间之和 + 自己的打水时间。打水时间短的人排在前面,后面等待的人受影响更小。

示例:t = [3, 1, 4]

排序后 [1, 3, 4]:等待时间 = 1 + (1+3) + (1+3+4) = 1+4+8 = 13

不排序 [3, 1, 4]:等待时间 = 3 + (3+1) + (3+1+4) = 3+4+8 = 15

题2活动安排实战

题目:以下 5 个活动,最多能参加几个?

活动开始结束
A13
B25
C47
D18
E69
查看参考答案

按结束时间排序:A[1,3) → B[2,5) → C[4,7) → D[1,8) → E[6,9)

① 选 A[1,3),last_end = 3

② B[2,5):开始 2 < 3,冲突 ✗

③ C[4,7):开始 4 ≥ 3,选!last_end = 7

④ D[1,8):开始 1 < 7,冲突 ✗

⑤ E[6,9):开始 6 < 7,冲突 ✗

答案:最多 2 个(A 和 C)

题3贪心正确性判断

题目:给定面值 {1, 5, 11},要找零 15 元。贪心策略"每次选最大面值"能得到最少硬币数吗?如果不能,给出反例。

查看参考答案

贪心失败!

贪心结果:11 + 1 + 1 + 1 + 1 = 5 枚

最优结果:5 + 5 + 5 = 3 枚 ✅

原因:面值 11 虽然是最大面值,但 15-11=4 需要 4 枚 1 元硬币,总共 5 枚。而 3 枚 5 元只需要 3 枚。

结论:这组面值不满足贪心选择性质,必须用动态规划(完全背包)求最优解。

模块 04

总结提升

1核心要点总结
知识板块必须记住的核心
贪心核心思想每步做局部最优选择,期望达到全局最优
两大适用条件贪心选择性质 + 最优子结构
贪心设计步骤确定策略 → 排序 → 逐一挑选 → 验证
活动选择按结束时间升序排序,选不与已选冲突的
分数背包按单位价值 v/w 降序排序,优先装性价比高的
合并果子每次合并最小两堆(优先队列/小根堆)
贪心失效场景0/1背包、非标准找零、加权活动选择 → 用DP
正确性验证交换论证 / 举反例 / 对拍验证
2拓展:贪心正确性的证明方法

💡 交换论证法(最常用的证明方法)

思路:假设贪心解为 G,最优解为 O。如果 G ≠ O,找到两者第一个不同的位置,证明可以通过"交换"使 O 变成 G 而解不会变差。

以活动选择为例:

设 O = {B₁, B₂, ...},G = {A₁, A₂, ...},且 A₁ ≠ B₁。

由于 A₁ 是结束最早的活动,A₁.e ≤ B₁.e。

用 A₁ 替换 B₁:因为 A₁ 更早结束,不会与后面的 B₂ 冲突(B₂.s ≥ B₁.e ≥ A₁.e)。

替换后仍是合法解,且数量不变 → 存在包含 A₁ 的最优解。

结论:"选结束最早的活动"这个贪心选择一定在某个最优解中。归纳可证贪心解整体最优。

3思考与启发

💡 学完本讲,你应该掌握的6项能力

1. 能识别一个问题是否适合用贪心法(关键看有没有后效性)。

2. 能设计贪心策略:确定排序关键字和选择规则。

3. 能用小样例验证贪心策略的正确性。

4. 能举出反例来否定错误的贪心策略。

5. 能使用优先队列实现合并果子等贪心算法。

6. 知道贪心失效时该切换到动态规划。

模块 05

学习检测

5 个初学者最常犯的错误。

易错1

所有问题都用贪心

❌ 错误
"贪心简单又快,什么问题都用贪心做。"
✅ 正确
贪心不是万能的。0/1背包、最长子序列等问题贪心得不到最优解,必须用动态规划。
口诀:贪心先验证,反例一票否决!
易错2

活动选择按"开始时间"排序

❌ 错误
"选开始最早的活动,这样能更早开始。"
✅ 正确
应按结束时间排序!开始早不代表结束早,可能占很长时间,挡住后面更多活动。
口诀:活动选择看"散场",谁先结束谁先上!
易错3

分数背包和 0/1 背包搞混

❌ 错误
"0/1 背包也可以用贪心法。"
✅ 正确
分数背包(物品可分割)→ 贪心;0/1 背包(物品不可分割)→ 动态规划。一字之差,方法完全不同。
口诀:可分用贪心,不可分用 DP!
易错4

合并果子每次合并最大两堆

❌ 错误
"每次合并最大的两堆,这样大堆早合并少算。"
✅ 正确
每次应合并最小的两堆!因为越早合并的堆,后面被"重复计算"的次数越多。小的先合并,总代价才最小。
口诀:合并选最小,小的先来代价少!
易错5

贪心正确性不验证就提交

❌ 错误
"感觉贪心是对的,直接写代码提交。"
✅ 正确
必须用小样例手动验证,或用对拍(贪心 vs DP/暴力)验证。竞赛中贪心写错就是 0 分。
口诀:贪心写码前,样例先过一遍!
速查贪心法速查表
经典问题贪心策略排序关键字时间复杂度
活动选择选结束最早且不冲突的结束时间 ↑O(n log n)
分数背包优先装性价比最高的v/w ↓O(n log n)
合并果子每次合并最小两堆小根堆O(n log n)
排队打水(SJF)用时短的先打水打水时间 ↑O(n log n)
区间覆盖选能覆盖最远的区间左端点 ↑O(n log n)
Huffman编码合并频率最小的两个节点小根堆O(n log n)
📌知识点笔记卡

🎯 贪心核心

每步局部最优

期望全局最优

需证明正确性

⭐ 两大条件

贪心选择性质

最优子结构

缺一不可

📺 活动选择

按结束时间排序

选不冲突的

O(n log n)

🎒 分数背包

按 v/w 降序

优先装性价比高的

可分割才用贪心

🍎 合并果子

每次选最小两堆

优先队列(小根堆)

O(n log n)

⚠️ 贪心失效

0/1背包 → DP

非标准找零 → DP

举反例一票否决

课后练习

分层练习作业

A基础 / B提高 / C挑战,难度递增,点击展开答案与解析。

🟢 A组 · 基础(4题)

A1选择题

贪心法的基本思想是()

A. 将问题分解为子问题,递归求解
B. 每步做出当前最优选择,不回溯
C. 枚举所有可能解,选最优
D. 从终点向起点逆推

查看参考答案

答案:B

贪心法的核心是"每步做当前最优选择,不回溯"。A 是分治/DP,C 是枚举,D 是逆推。

考点:贪心法的核心是“每步选取当前看来最优的方案,做出选择后不回溯、不撤销”。

易错:把“局部最优”直接等同于“全局最优”,忽略了贪心需要正确性证明才能放心使用。

A2选择题

活动选择问题中,正确的贪心策略是()

A. 优先选择开始时间最早的活动
B. 优先选择持续时间最短的活动
C. 优先选择结束时间最早的活动
D. 优先选择持续时间最长的活动

查看参考答案

答案:C. 优先选择结束时间最早的活动

结束越早,留给后面活动的时间越多,可以安排更多活动。可以举反例排除 B:[1,10) vs [9,11),前者持续9但开始早,后者持续2但结束晚。

考点:按结束时间排序能让剩余可用时间最多,是活动选择问题的标准贪心策略。

易错:改成按“开始时间最早”或“持续时间最短”排序,这两种策略都能举出反例推翻。

A3选择题

分数背包问题中,正确的贪心策略是()

A. 优先装价值最大的物品
B. 优先装重量最小的物品
C. 优先装单位价值(v/w)最大的物品
D. 优先装体积最大的物品

查看参考答案

答案:C. 优先装单位价值(v/w)最大的物品

单位价值最高意味着"每占一单位重量带来的价值最大",这样可以最大化背包的总价值。

考点:分数背包允许物品分割,按单位价值 v/w 从大到小装填即可得到最优解。

易错:把分数背包的贪心策略照搬到 0/1 背包上——0/1 背包物品不可分割,贪心会得到次优解。

A4编程题

输入 n 个活动的开始时间和结束时间,用贪心法求最多能参加多少个活动。

查看参考答案
#include <iostream>
#include <algorithm>
using namespace std;
struct Act { int s, e; };
bool cmp(Act a, Act b) { return a.e < b.e; }
int main() {
    int n; cin >> n;
    Act a[1005];
    for(int i=0;i<n;i++) cin >> a[i].s >> a[i].e;
    sort(a, a+n, cmp);
    int cnt=0, last=0;
    for(int i=0;i<n;i++)
        if(a[i].s >= last) { cnt++; last = a[i].e; }
    cout << cnt << endl;
    return 0;
}

思路:先按结束时间升序排序,再从头扫描,只要活动开始时间不早于上一个选中活动的结束时间就选它。

易错:比较函数写成按开始时间排序(a.slastEnd 把 s==lastEnd 的合法活动也拒绝掉。

🟡 B组 · 提高(4题)

B1编程题

合并果子:n 堆果子,每次合并两堆,代价为两堆之和。求合并成一堆的最小总代价。

查看参考答案
#include <iostream>
#include <queue>
using namespace std;
int main() {
    int n; cin >> n;
    priority_queue<int,vector<int>,greater<int>> pq;
    for(int i=0;i<n;i++){int x;cin>>x;pq.push(x);}
    long long ans=0;
    while(pq.size()>1){
        int a=pq.top();pq.pop();
        int b=pq.top();pq.pop();
        ans+=a+b; pq.push(a+b);
    }
    cout << ans << endl;
    return 0;
}

复杂度:O(n log n),优先队列每次操作 O(log n)。

思路:用小根堆每次弹出最小的两堆合并,把合并后的新堆放回堆中,累计代价就是哈夫曼思想的最优解。

易错:合并后忘记把新堆 push 回优先队列;或用 int 累加总代价,n 较大时会溢出(总和可达 10¹⁰ 量级)。

B2编程题

排队打水:n 个人,第 i 个人打水需要 tᵢ 分钟。安排顺序使所有人等待时间之和最小,输出最小等待时间之和。

查看参考答案
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
    int n; cin >> n;
    int t[1005];
    for(int i=0;i<n;i++) cin >> t[i];
    sort(t, t+n);  // 短作业优先
    long long total=0, sum=0;
    for(int i=0;i<n;i++){
        sum += t[i];
        total += sum;
    }
    cout << total << endl;
    return 0;
}

原理:第 i 个人打水 t[i] 分钟,后面 n-i 个人都要多等 t[i]。所以 t[i] 的贡献 = t[i] × (n-i)。让小的 t[i] 在前面,总贡献最小。

思路:短作业优先——按打水时间升序排序,用前缀和逐步累加,得到所有人的等待时间之和最小。

易错:忽略“前面所有人的打水时间都会被后面的人重复等待”这一效应;或用 int 存总和在 n 较大时溢出。

B3正确性分析

面值 {1, 3, 4},找零 6 元。证明贪心策略(每次选最大面值)不是最优的,并给出正确的 DP 思路。

查看参考答案

反例:

贪心:4 + 1 + 1 = 3 枚

最优:3 + 3 = 2 枚

贪心得不到最优解,因此该面值集合不满足贪心选择性质。

DP 思路:设 dp[i] 表示找零 i 元的最少硬币数。

dp[i] = min(dp[i-1], dp[i-3], dp[i-4]) + 1

dp[0]=0, dp[1]=1, dp[2]=2, dp[3]=1, dp[4]=1, dp[5]=2, dp[6]=2

dp[6] = min(dp[5], dp[3], dp[2]) + 1 = min(2, 1, 2) + 1 = 2 ✅

易错:以为“每次选最大面值”对任意面值集合都成立——缺少合适倍数关系时(如 {1,3,4})贪心会出错,判断能否贪心要看有没有反例。

B4编程题

区间覆盖:给定线段 [0, M] 和 n 个区间 [lᵢ, rᵢ],问最少需要多少个区间才能完全覆盖 [0, M]。如果无法覆盖输出 -1。

查看参考答案
#include <iostream>
#include <algorithm>
using namespace std;
struct Seg { int l, r; };
bool cmp(Seg a, Seg b) { return a.l < b.l; }
int main() {
    int n, M; cin >> n >> M;
    Seg s[10005];
    for(int i=0;i<n;i++) cin >> s[i].l >> s[i].r;
    sort(s, s+n, cmp);
    int cnt=0, cover=0, i=0;
    while(cover < M) {
        int maxR = cover;
        while(i < n && s[i].l <= cover) {
            maxR = max(maxR, s[i].r);
            i++;
        }
        if(maxR == cover) { cout << -1; return 0; }
        cover = maxR;
        cnt++;
    }
    cout << cnt << endl;
    return 0;
}

贪心策略:按左端点排序。每次在能覆盖当前位置的所有区间中,选右端点最远的那个。

思路:按左端点排序,维护“已覆盖到的最右端 cur”,每轮在所有 l≤cur 的区间中挑 r 最大的那个,把 cur 推远。

易错:按区间长度排序或每次只接第一个可用区间;漏判“出现空隙”的情况,既不输出 -1 又陷入死循环。

🔴 C组 · 挑战(4题)

C1初赛真题

关于贪心法,下列说法正确的是()

A. 贪心法总能得到全局最优解
B. 贪心法适用于所有最优化问题
C. 贪心法每步做出局部最优选择,不回溯
D. 贪心法的时间复杂度一定比动态规划低

查看参考答案

答案:C

A 错(只在满足贪心性质时得到全局最优),B 错(0/1背包等不适用),D 错(不一定)。C 正确描述了贪心法的核心特征。

考点:贪心算法的正确性依赖贪心选择性质与最优子结构,且只对一部分问题成立。

易错:认为“贪心一定能得到全局最优”,或认为“DP 能解的问题贪心也一定能解”。

C2初赛真题

n 个活动按结束时间排序后,用贪心法选择活动的时间复杂度为()

A. O(n)   B. O(n log n)   C. O(n²)   D. O(2ⁿ)

查看参考答案

答案:B. O(n log n)

排序 O(n log n) + 扫描选择 O(n) = O(n log n)。排序是瓶颈。

考点:贪心的时间开销主要由排序决定,扫描选择部分是线性的,整体取较大者。

易错:把扫描比较那一层也当成 O(n log n),或直接忽略排序答成 O(n)。

C3综合编程

CSP-J 改编:有 n 根木棍,长度分别为 a₁, a₂, ..., aₙ。需要将它们分成两组,使两组长度之和的差最小。请分析能否用贪心法解决,如果不能,应该用什么方法?

查看参考答案
C++完整参考代码
#include <iostream>
#include <vector>
#include <cstdlib>
#include <functional>
#include <algorithm>
using namespace std;

int main() {
    int a[] = {3, 3, 2, 2, 2};
    int n = 5, sum = 0;
    for (int i = 0; i < n; i++) sum += a[i];

    // 方法一:贪心——从大到小,每次把木棍放进当前较短的一组
    vector<int> v(a, a + n);
    sort(v.begin(), v.end(), greater<int>());
    int g1 = 0, g2 = 0;
    for (int i = 0; i < n; i++) {
        if (g1 <= g2) g1 += v[i];
        else          g2 += v[i];
    }
    cout << "贪心分组: " << g1 << " vs " << g2
         << "   差 = " << abs(g1 - g2) << endl;

    // 方法二:0/1 背包 DP——dp[j] 表示能否凑出长度和恰好为 j
    vector<bool> dp(sum + 1, false);
    dp[0] = true;
    for (int i = 0; i < n; i++)
        for (int j = sum; j >= a[i]; j--)          // 必须倒着更新
            if (dp[j - a[i]]) dp[j] = true;

    int best = 0;
    for (int j = sum / 2; j >= 0; j--)             // 找不超过一半的最大可达和
        if (dp[j]) { best = j; break; }

    cout << "最优分组: " << best << " vs " << sum - best
         << "   差 = " << sum - 2 * best << endl;
    return 0;
}

答案:不能直接用贪心法。反例 [3, 3, 2, 2, 2]:贪心(从大到小轮流放进较短的一组)得到 {3, 2, 2} = 7 与 {3, 2} = 5,差 = 2;而最优分组是 {3, 3} = 6 与 {2, 2, 2} = 6,差 = 0。正确方法是 0/1 背包 DP:目标容量取 sum / 2,用 dp[j] 表示能否选出长度之和恰好为 j 的木棍子集,最终答案 = sum − 2 × (不超过 sum/2 的最大可达 j),时间复杂度 O(n × sum)。

思路:两组之差最小,等价于让其中一组尽量接近总和的一半。贪心只在「每一步都选当前最好」时才会全局最优,而本题某根木棍一旦选错组就无法回退——反例正说明贪心会栽在这种「局部最优 ≠ 全局最优」的场合。改成 DP 后,dp[j] 记录所有可能的和,不漏掉任何组合,所以一定得到最优解。

考点:贪心法的适用条件(有没有反例);0/1 背包 DP 的建模与滚动数组实现;sum − 2×best 这种「差值转凑和」的技巧。

易错:① 滚动数组里内层循环 j 必须从大到小(倒序),否则同一根木棍会被重复使用,变成「完全背包」;② 目标容量是 sum/2,要从 sum/2 往下找第一个可达的 j,而不是从 0 往上;③ 别用 sum/2 当答案,忘记乘 2 —— 差值公式是 sum − 2 × best;④ n 较小或 sum 很大时 DP 会偏慢,此时可改用 meet-in-the-middle 折半枚举。

C4综合编程

NOIP 真题改编:国王有 n 个大臣,每个大臣左右手各写了一个整数 Lᵢ 和 Rᵢ。国王让大臣排成一列,国王的左手写的是 A。每个大臣获得的金币 = 前面所有人左手数字之积 / 自己右手数字。求如何排列使获得金币最多的大臣得到的金币最少。

查看参考答案

贪心策略:按 Lᵢ × Rᵢ 从小到大排序。

证明思路(交换论证):

考虑相邻的两个大臣 i 和 j(i 在 j 前面),设前面所有人的左手之积为 P。

i 在前时:i 得 P/Rᵢ,j 得 P×Lᵢ/Rⱼ

j 在前时:j 得 P/Rⱼ,i 得 P×Lⱼ/Rᵢ

如果 i 在前的最大值 ≤ j 在前的最大值:

max(P/Rᵢ, P×Lᵢ/Rⱼ) ≤ max(P/Rⱼ, P×Lⱼ/Rᵢ)

化简得:Lᵢ×Rᵢ ≤ Lⱼ×Rⱼ

所以按 L×R 升序排列是最优的。

struct Minister {
    long long L, R;
};
bool cmp(Minister a, Minister b) {
    return a.L * a.R < b.L * b.R;  // 按 L×R 升序
}

注意:由于左手数字之积可能非常大,需要用高精度运算!