目录
一、什么是贪心算法
1.1 贪心策略
1.2 贪心 vs 动态规划
二、经典问题一:活动选择问题
2.1 问题描述
2.2 贪心证明思路
2.3 代码实现
2.4 复杂度分析
三、经典问题二:找零问题
3.1 问题描述
3.2 贪心的局限性
3.3 代码实现(贪心)
四、贪心算法的正确性条件
4.1 贪心选择性质
4.2 最优子结构
五、其他贪心算法经典问题
六、贪心算法解题模板
七、贪心 vs DP 选择指南
八、小结
九、思考题
一、什么是贪心算法
1.1 贪心策略
贪心算法在每一步都选择当前状态下的最优解(局部最优),而不考虑未来的影响。
特点:
实现简单,通常很快
不能保证对所有问题都得到全局最优解
需要证明问题具有贪心选择性质
1.2 贪心 vs 动态规划
| 对比项 | 贪心算法 | 动态规划 |
|---|---|---|
| 决策方式 | 单次决策,不回头 | 考虑所有可能性 |
| 子问题重叠 | 不一定 | 必然 |
| 典型问题 | 活动选择、找零 | 背包、LCS |
| 时间复杂度 | 通常 O(n) 或 O(n log n) | 通常 O(n²) 或更高 |
| 全局最优保证 | 需要证明 | 总是保证 |
二、经典问题一:活动选择问题
2.1 问题描述
有 n 个活动,每个活动有开始时间s[i]和结束时间f[i]。活动不能同时进行(时间不能重叠)。求最多能安排多少个活动。
贪心策略:每次选择结束时间最早且与已选活动不冲突的活动。
2.2 贪心证明思路
为什么选结束时间最早的是最优?
选结束时间最早的活动,能为剩余活动留出最多的时间
这个选择不会比任何最优解差
2.3 代码实现
c
#include <stdio.h> #include <stdlib.h> // 活动结构体 typedef struct { int start; int finish; } Activity; // 按结束时间排序(升序) int cmp(const void *a, const void *b) { return ((Activity*)a)->finish - ((Activity*)b)->finish; } // 贪心算法选择活动 int activitySelection(Activity activities[], int n) { if (n == 0) return 0; // 1. 按结束时间排序 qsort(activities, n, sizeof(Activity), cmp); // 2. 贪心选择 int count = 1; int lastFinish = activities[0].finish; printf("选中的活动: (0, %d)\n", activities[0].finish); for (int i = 1; i < n; i++) { if (activities[i].start >= lastFinish) { printf("选中的活动: (%d, %d)\n", activities[i].start, activities[i].finish); count++; lastFinish = activities[i].finish; } } return count; } int main() { Activity activities[] = { {1, 4}, {3, 5}, {0, 6}, {5, 7}, {3, 9}, {5, 9}, {6, 10}, {8, 11}, {8, 12}, {2, 14}, {12, 16} }; int n = sizeof(activities) / sizeof(activities[0]); int result = activitySelection(activities, n); printf("最多可安排活动数: %d\n", result); return 0; }运行结果:
text
选中的活动: (0, 4) 选中的活动: (5, 7) 选中的活动: (8, 11) 选中的活动: (12, 16) 最多可安排活动数: 4
2.4 复杂度分析
排序:O(n log n)
贪心选择:O(n)
总时间复杂度:O(n log n)
三、经典问题二:找零问题
3.1 问题描述
给定面额数组coins(假设无限供应)和金额amount,求最少需要多少枚硬币。
贪心策略:每次用最大面额不超过剩余金额的硬币。
3.2 贪心的局限性
贪心算法不一定对所有硬币系统都正确。
| 硬币系统 | 金额 | 贪心结果 | 最优结果 | 是否正确 |
|---|---|---|---|---|
| 1,5,10,25(标准) | 30 | 25+5 → 2枚 | 10+10+10 → 3枚 | ✓ |
| 1,3,4 | 6 | 4+1+1 → 3枚 | 3+3 → 2枚 | ✗ |
3.3 代码实现(贪心)
c
#include <stdio.h> #include <stdlib.h> // 按面额降序排序 int cmpDesc(const void *a, const void *b) { return *(int*)b - *(int*)a; } // 贪心找零 void greedyCoinChange(int coins[], int n, int amount) { qsort(coins, n, sizeof(int), cmpDesc); printf("贪心找零 %d 元: ", amount); int remaining = amount; for (int i = 0; i < n && remaining > 0; i++) { if (remaining >= coins[i]) { int count = remaining / coins[i]; printf("%d枚%d元 ", count, coins[i]); remaining -= count * coins[i]; } } if (remaining > 0) { printf("无法找零"); } printf("\n"); } int main() { int coins1[] = {1, 5, 10, 25}; int coins2[] = {1, 3, 4}; greedyCoinChange(coins1, 4, 30); greedyCoinChange(coins2, 3, 6); return 0; }运行结果:
text
贪心找零 30 元: 1枚25元 1枚5元 贪心找零 6 元: 1枚4元 2枚1元
四、贪心算法的正确性条件
4.1 贪心选择性质
定义:问题的全局最优解可以通过一系列局部最优选择(贪心选择)得到。
证明方法:
假设有一个最优解
证明可以将其改造为包含贪心选择的解
用数学归纳法或反证法
4.2 最优子结构
定义:问题的最优解包含子问题的最优解。
活动选择问题满足最优子结构:去掉贪心选择的活动后,剩余子问题的最优解加上这个活动就是全局最优解。
五、其他贪心算法经典问题
| 问题 | 贪心策略 | 时间复杂度 |
|---|---|---|
| 哈夫曼编码 | 每次合并频率最小的两个节点 | O(n log n) |
| 最小生成树(Prim/Kruskal) | 每次选最小权边 | O(E log V) |
| 最短路径(Dijkstra) | 每次选距离最小的顶点 | O(V²) 或 O(E log V) |
| 区间覆盖 | 每次选能覆盖最远右端点的区间 | O(n log n) |
| 任务调度(最小化总完成时间) | 按处理时间升序 | O(n log n) |
六、贪心算法解题模板
c
// 贪心算法通用框架 void greedy() { // 1. 按某种规则排序(如果需要) sort(data); // 2. 初始化结果 result = initial; // 3. 贪心选择 for (each element in sorted order) { if (符合条件) { 选择当前元素; 更新状态; } } }七、贪心 vs DP 选择指南
| 问题特征 | 推荐算法 |
|---|---|
| 可以证明贪心选择性质 | 贪心(更高效) |
| 需要考虑多种可能性 | DP |
| 子问题重叠明显 | DP |
| 只需局部最优决策 | 贪心 |
| 可分解为独立子问题 | 分治或贪心 |
判断方法:
尝试贪心策略,看能否举出反例
如果不能,考虑DP
如果DP太慢,尝试证明贪心正确
八、小结
这一篇我们学习了贪心算法:
| 要点 | 说明 |
|---|---|
| 核心思想 | 每次选当前最优,希望达到全局最优 |
| 活动选择 | 选结束时间最早的,复杂度 O(n log n) |
| 找零问题 | 用最大面额优先,但不一定正确 |
| 正确性条件 | 贪心选择性质 + 最优子结构 |
| 适用范围 | 图论(MST、最短路)、调度、哈夫曼编码 |
贪心 vs DP:
贪心:局部最优 → 全局最优(需要证明)
DP:考虑所有可能性 → 全局最优(保证正确)
学习建议:
多练习判断问题是否适合贪心
学会举反例推翻错误贪心策略
掌握常见贪心问题(活动选择、哈夫曼、区间覆盖)
下一篇我们讲代码调试技巧与常见内存错误排查。
九、思考题
活动选择问题中,如果改成选开始时间最早或持续时间最短,能得到最优解吗?为什么?
硬币系统 {1, 5, 10, 25} 中贪心总是正确的,但 {1, 3, 4} 中不对。硬币系统满足什么条件时贪心正确?
如何用贪心算法解决“用最少的箭射爆所有气球”问题?
如果活动有时间权重(每个活动有不同价值),如何用DP解决?
欢迎在评论区讨论你的答案。