GESP 5级
简单贪心
5级 · 基础算法与数据类型
📖 什么是贪心算法?
贪心算法(Greedy Algorithm)的思路非常直觉:每一步都做当前看起来最好的选择,期望最终得到全局最优解。
生活类比:你去自动售货机买饮料,目标是"花最少的钱买到想喝的"。贪心策略就是:每次都选最便宜的那个,不去想"如果这次贵一点,下次可能会更便宜"——只看眼前,不管以后。
贪心的关键问题:什么时候贪心能得到正确答案?
能用贪心的情况:每一步的"局部最优"确实能导致"全局最优"
不能用贪心的情况:当前的"最优"可能影响后面更大的收益
GESP 5级考的是"简单贪心"——题目设计好了就是能用贪心的,不用担心判断能否用。
不能用贪心的情况:当前的"最优"可能影响后面更大的收益
GESP 5级考的是"简单贪心"——题目设计好了就是能用贪心的,不用担心判断能否用。
🌟 为什么重要?
- 简单高效:贪心通常只需要排序 + 一次遍历,时间复杂度 O(n log n),非常快。
- 竞赛常见:活动选择、区间调度、分糖果、跳游戏等经典贪心题在 GESP 和竞赛中频繁出现。
- 思维训练:贪心需要你找到"正确的贪心策略",这培养了分析问题的能力。
- 与其他算法结合:贪心常与排序、优先队列配合使用,是工具箱中不可缺少的一环。
📋 前置知识
- ✅ 数组的基本操作
- ✅ for 循环
- ✅ 排序(
sort()函数、自定义比较函数) - ✅ 结构体的定义和使用
🎯 例1:活动选择问题(经典贪心)
问题:有 n 个活动,每个活动有开始时间和结束时间。一个人同一时间只能参加一个活动。问最多能参加多少个活动?
贪心策略:每次选结束时间最早的活动——这样能尽早腾出时间给后面的活动。
为什么这个策略有效?
结束越早 → 留给后面的空闲时间越多 → 能容纳更多活动
就像"先做完短任务"能让一天安排更多事。
就像"先做完短任务"能让一天安排更多事。
手算示例:
活动列表(按结束时间排序后):
A: [1, 3] B: [2, 5] C: [4, 7] D: [6, 8] E: [5, 9]
1. 选 A [1,3] → cnt=1, last_end=3
2. B [2,5] 的 start=2 < last_end=3 → 冲突,跳过
3. C [4,7] 的 start=4 >= last_end=3 → 选 C, cnt=2, last_end=7
4. D [6,8] 的 start=6 < last_end=7 → 冲突,跳过
5. E [5,9] 的 start=5 < last_end=7 → 冲突,跳过
最终选了 2 个活动:A 和 C
A: [1, 3] B: [2, 5] C: [4, 7] D: [6, 8] E: [5, 9]
1. 选 A [1,3] → cnt=1, last_end=3
2. B [2,5] 的 start=2 < last_end=3 → 冲突,跳过
3. C [4,7] 的 start=4 >= last_end=3 → 选 C, cnt=2, last_end=7
4. D [6,8] 的 start=6 < last_end=7 → 冲突,跳过
5. E [5,9] 的 start=5 < last_end=7 → 冲突,跳过
最终选了 2 个活动:A 和 C
💻 活动选择问题
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 定义活动结构体
struct Activity {
int start; // 开始时间
int end; // 结束时间
};
// 自定义排序规则:按结束时间从小到大排
bool cmp(Activity a, Activity b) {
return a.end < b.end; // 结束时间早的排前面
}
int greedy(vector<Activity>& acts) {
// 第1步:按结束时间排序(贪心的基础)
sort(acts.begin(), acts.end(), cmp);
int cnt = 1; // 选了第1个活动
int last_end = acts[0].end; // 记录上一个选中活动的结束时间
// 第2步:从第2个活动开始遍历
for (int i = 1; i < acts.size(); i++) {
// 如果当前活动的开始时间 >= 上一个活动的结束时间
// 说明不冲突,可以选
if (acts[i].start >= last_end) {
cnt++; // 计数 +1
last_end = acts[i].end; // 更新结束时间
}
// 否则冲突了,跳过这个活动
}
return cnt; // 返回最多能选多少个活动
}
int main() {
vector<Activity> acts = {{1,3}, {2,5}, {4,7}, {6,8}, {5,9}};
cout << "最多能参加 " << greedy(acts) << " 个活动" << endl;
// 输出: 最多能参加 2 个活动
return 0;
}
🎯 例2:分糖果问题
问题:有 n 个孩子和 n 颗糖果。每个孩子有一个"满足度"需求,每颗糖果有一个"甜度"。一个孩子只能吃一颗糖果,当糖果甜度 ≥ 孩子需求时才算满足。问最多能满足多少个孩子?
贪心策略:把孩子需求和糖果甜度都从小到大排序,然后用最小的糖果满足最小的需求——这样不浪费大糖果。
💻 分糖果
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int findContentChildren(vector<int>& g, vector<int>& s) {
// g: 孩子的需求(满足度)数组
// s: 糖果的甜度数组
// 都从小到大排序
sort(g.begin(), g.end());
sort(s.begin(), s.end());
int child = 0; // 指向当前要满足的孩子
int candy = 0; // 指向当前要分配的糖果
int count = 0; // 已满足的孩子数
// 从需求最小的孩子开始,用甜度最小的糖果尝试满足
while (child < g.size() && candy < s.size()) {
if (s[candy] >= g[child]) {
// 这颗糖果够甜,满足这个孩子
count++;
child++; // 下一个孩子
candy++; // 下一颗糖果
} else {
// 这颗糖果不够甜,试试下一颗更大的糖果
candy++;
}
}
return count;
}
int main() {
vector<int> children = {1, 2, 3}; // 孩子需求
vector<int> candies = {1, 1}; // 糖果甜度
cout << "满足 " << findContentChildren(children, candies) << " 个孩子" << endl;
// 输出: 满足 1 个孩子(只有第一个孩子的需求1可以被糖果1满足)
vector<int> children2 = {1, 2};
vector<int> candies2 = {1, 2, 3};
cout << "满足 " << findContentChildren(children2, candies2) << " 个孩子" << endl;
// 输出: 满足 2 个孩子
return 0;
}
📐 贪心的一般步骤
📐 写贪心算法的三步法
第1步:确定贪心策略
"每一步选什么最优?" → 排序的依据是什么?
活动选择:按结束时间排 → 选结束最早的
分糖果:按需求/甜度排 → 用最小的满足最小的
第2步:排序
根据贪心策略,对数据进行排序
常用 sort() + 自定义比较函数
第3步:一次遍历
排序后,从头到尾遍历,每一步做"当前最优选择"
通常只需一个 for 循环 + 几个变量
"每一步选什么最优?" → 排序的依据是什么?
活动选择:按结束时间排 → 选结束最早的
分糖果:按需求/甜度排 → 用最小的满足最小的
第2步:排序
根据贪心策略,对数据进行排序
常用 sort() + 自定义比较函数
第3步:一次遍历
排序后,从头到尾遍历,每一步做"当前最优选择"
通常只需一个 for 循环 + 几个变量
🎯 例3:跳游戏(贪心判断能否到达终点)
问题:数组 nums[i] 表示从位置 i 最远能跳几步。判断能否从起点跳到终点。
贪心策略:维护一个"目前能到达的最远位置",每一步都更新它。
💻 跳游戏
#include <iostream>
#include <vector>
using namespace std;
bool canJump(vector<int>& nums) {
int maxReach = 0; // 目前能到达的最远位置
for (int i = 0; i < nums.size(); i++) {
// 如果当前位置已经超过能到达的最远位置,说明走不动了
if (i > maxReach) return false;
// 更新最远能到达的位置
maxReach = max(maxReach, i + nums[i]);
}
return true; // 遍历完了都能走到,说明能到终点
}
int main() {
vector<int> nums1 = {2, 3, 1, 1, 4};
cout << (canJump(nums1) ? "能到达" : "不能到达") << endl;
// 输出: 能到达
vector<int> nums2 = {3, 2, 1, 0, 4};
cout << (canJump(nums2) ? "能到达" : "不能到达") << endl;
// 输出: 不能到达(在位置3卡住了,nums[3]=0)
return 0;
}
⚠️ 易错点
🚨 常见错误
- 贪心策略选错:比如活动选择问题,如果按"开始时间排序"而不是"结束时间排序",结果就不是最优的。选择正确的贪心策略是关键。
- 忘记排序:贪心的前提是数据有序!很多初学者直接遍历就贪心了,但没排序的话贪心策略不成立。
- 排序后索引混乱:排序会改变元素的位置。排序后要记住元素的原始信息,别搞混了。
- 不是所有问题都能贪心:贪心不是万能的。比如"0-1背包问题"就不能用贪心。GESP 5级考的是简单贪心,题目通常能贪,但到了更高级别要会判断。
- 边界条件:空数组、只有一个元素等特殊情况要单独考虑。排序前检查数组是否为空。
- 整数溢出:计算"最远到达位置"时,
i + nums[i]可能溢出。在竞赛中一般不会卡这个,但要注意。
📝 练习建议
💡 怎么练?
- 先理解"为什么这样贪":做每道贪心题,先想清楚"为什么按这个顺序选能得到最优解",不要死记模板。
- 手算验证:在纸上用小数据手动模拟贪心过程,验证结果是否正确。
- 从经典题开始:活动选择 → 分糖果 → 跳游戏 → 区间覆盖 → 任务调度,循序渐进。
- 练习排序策略:同一道题,试试不同的排序方式,看哪个能得到正确答案。
- 推荐题目:洛谷 P1803(凌乱的yyy/活动选择)、LeetCode 455(分糖果)、LeetCode 55(跳游戏)、LeetCode 435(无重叠区间)。