GESP 5级

简单贪心

5级 · 基础算法与数据类型

📖 什么是贪心算法?

贪心算法(Greedy Algorithm)的思路非常直觉:每一步都做当前看起来最好的选择,期望最终得到全局最优解。

生活类比:你去自动售货机买饮料,目标是"花最少的钱买到想喝的"。贪心策略就是:每次都选最便宜的那个,不去想"如果这次贵一点,下次可能会更便宜"——只看眼前,不管以后。

贪心的关键问题:什么时候贪心能得到正确答案?

能用贪心的情况:每一步的"局部最优"确实能导致"全局最优"
不能用贪心的情况:当前的"最优"可能影响后面更大的收益

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
💻 活动选择问题
#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 循环 + 几个变量

🎯 例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;
}

⚠️ 易错点

🚨 常见错误
  1. 贪心策略选错:比如活动选择问题,如果按"开始时间排序"而不是"结束时间排序",结果就不是最优的。选择正确的贪心策略是关键。
  2. 忘记排序:贪心的前提是数据有序!很多初学者直接遍历就贪心了,但没排序的话贪心策略不成立。
  3. 排序后索引混乱:排序会改变元素的位置。排序后要记住元素的原始信息,别搞混了。
  4. 不是所有问题都能贪心:贪心不是万能的。比如"0-1背包问题"就不能用贪心。GESP 5级考的是简单贪心,题目通常能贪,但到了更高级别要会判断。
  5. 边界条件:空数组、只有一个元素等特殊情况要单独考虑。排序前检查数组是否为空。
  6. 整数溢出:计算"最远到达位置"时,i + nums[i] 可能溢出。在竞赛中一般不会卡这个,但要注意。

📝 练习建议

💡 怎么练?
  1. 先理解"为什么这样贪":做每道贪心题,先想清楚"为什么按这个顺序选能得到最优解",不要死记模板。
  2. 手算验证:在纸上用小数据手动模拟贪心过程,验证结果是否正确。
  3. 从经典题开始:活动选择 → 分糖果 → 跳游戏 → 区间覆盖 → 任务调度,循序渐进。
  4. 练习排序策略:同一道题,试试不同的排序方式,看哪个能得到正确答案。
  5. 推荐题目:洛谷 P1803(凌乱的yyy/活动选择)、LeetCode 455(分糖果)、LeetCode 55(跳游戏)、LeetCode 435(无重叠区间)。