目录
[1.1 贪心策略](#1.1 贪心策略)
[1.2 贪心 vs 动态规划](#1.2 贪心 vs 动态规划)
[2.1 问题描述](#2.1 问题描述)
[2.2 贪心证明思路](#2.2 贪心证明思路)
[2.3 代码实现](#2.3 代码实现)
[2.4 复杂度分析](#2.4 复杂度分析)
[3.1 问题描述](#3.1 问题描述)
[3.2 贪心的局限性](#3.2 贪心的局限性)
[3.3 代码实现(贪心)](#3.3 代码实现(贪心))
[4.1 贪心选择性质](#4.1 贪心选择性质)
[4.2 最优子结构](#4.2 最优子结构)
[七、贪心 vs DP 选择指南](#七、贪心 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解决?
欢迎在评论区讨论你的答案。