一、本质与核心思想
贪心算法(Greedy Algorithm)是一种局部最优导向全局最优的算法范式,其核心逻辑是:
在每一步决策时,仅考虑当前状态下的最优选择,而不回溯或考虑未来影响
这种"目光短浅"的特性带来两个关键特征:
- 高效性:通常时间复杂度为 O(n) 或 O(n log n)
- 风险性:不一定获得全局最优解(需问题满足特定性质)
二、算法框架与流程
详细步骤解析:
问题分解
将问题转化为一系列连续决策点(如找零问题中的每次硬币选择)定义贪心策略
确定"最优"的量化标准,常见策略:- 最大值优先(背包问题)
- 最小值优先(哈夫曼编码)
- 最早结束(活动选择)
- 最晚开始(任务调度)
数据预处理
80%的贪心问题需要先排序(时间复杂度 O(n log n))迭代选择
- 根据贪心策略选择当前最优项
- 检查选择是否满足约束条件
- 更新问题状态
- 移出已处理项
while 未达到终止条件: 候选集 = 获取当前可选项目() 最优项 = 根据贪心策略选择(候选集) if 最优项满足约束条件: 加入最终解() 更新问题状态() 移除已考虑项(最优项)
终止条件
- 问题状态达到目标值
- 候选集为空
- 无法继续满足约束
三、经典应用场景与实例分析
场景1:找零问题(硬币最小化)
问题:用最少的硬币找零,硬币面额:25¢, 10¢, 5¢, 1¢,找零41¢
策略:每次选择不超过剩余金额的最大面值硬币
执行过程:
步骤 | 选择 | 剩余 | 硬币累计 |
---|---|---|---|
1 | 25¢ | 16¢ | [25] |
2 | 10¢ | 6¢ | [25,10] |
3 | 5¢ | 1¢ | [25,10,5] |
4 | 1¢ | 0¢ | [25,10,5,1] |
解:[25,10,5,1](4枚硬币)
⚠️ 陷阱案例:面值[1,3,4]¢找零6¢
贪心解:4+1+1(3枚)
实际最优:3+3(2枚)
失败原因:不满足贪心选择性质
场景2:活动选择问题
问题:从一组有重叠时间的活动中,选择最多的互不冲突活动(选择最多数量的互不重叠活动)
策略:优先选择最早结束的活动
数据:
活动 | 开始 | 结束 |
---|---|---|
A | 1 | 4 |
B | 3 | 5 |
C | 0 | 6 |
D | 5 | 7 |
E | 3 | 9 |
执行过程:
- 按结束时间排序:A(4), B(5), D(7), C(6), E(9)
- 选A(结束最早)
- 跳过B(与A重叠)
- 选D(不重叠)
- 结果:A和D(2个活动)
场景3:霍夫曼编码(数据压缩)
问题:设计最优二进制前缀码,实现数据压缩
贪心策略:每次合并频率最低的两个节点
数据:A:15, B:7, C:6, D:6
字符频率:
A:15, B:7, C:6, D:6
过程:
编码结果:A:1, B:00, C:010, D:011
场景4:最小生成树(Prim算法)
问题:连通所有节点,使总边权最小
贪心策略:每次添加连接树与非树节点的最小边
执行过程:
总权重:2+3+1=6(全局最优)
四、正确性证明:两个关键性质
贪心算法有效的充要条件:
贪心选择性质 (Greedy Choice Property)
证明:存在最优解包含当前贪心选择例:活动选择中,最早结束活动必在某个最优解中
最优子结构 (Optimal Substructure)
证明:做出贪心选择后,剩余问题与原问题同构例:背包问题中,选择物品后的剩余容量仍是背包问题
验证方法:
- 证明安全选择:局部最优解总是全局最优解的一部分
- 数学归纳法:证明每个步骤的选择保持最优性
- 交换论证:通过比较解差异证明最优性
五、算法模板(Python实现)
def greedy_algorithm(items):
# 1. 预处理(通常排序)
items.sort(key=lambda x: x.value) # 按贪心策略排序
solution = [] # 存储解
current_state = initial_state # 初始状态
# 2. 迭代选择
for item in items:
if is_feasible(item, current_state):
solution.append(item)
current_state = update_state(current_state, item) # 更新状态
# 提前终止检查
if is_terminal(current_state):
break
return solution
六、适用场景
优势与局限性
优势 | 局限性 |
---|---|
时间复杂度低(常O(n)) | 不能保证全局最优 |
实现简单 | 需严格证明问题性质 |
空间复杂度低 | 选择不可逆 |
适合实时系统 | 对问题结构要求严格 |
适用场景
场景特征 | 适合贪心 | 不适合贪心 |
---|---|---|
局部最优可导向全局最优 | ✓ | ✗ |
问题具有最优子结构 | ✓ | ✗ |
选择不可逆 | ✓ | ✗ |
需要精确全局最优 | ✗ | ✓(用动态规划) |
问题有后效性 | ✗ | ✓(用回溯/动态规划) |
七、技巧与常见错误
高效技巧:
优先级队列优化:动态获取最优项(Dijkstra算法)
import heapq heap = [] heapq.heappush(heap, (priority, item))
早停机制:满足条件立即终止循环
增量计算:避免重复遍历数据
常见错误:
# 错误1:忘记排序
items = get_items() # 缺少排序将导致策略失效
# 错误2:未验证约束
if greedy_condition: # 必须检查是否满足问题约束
select(item)
# 错误3:修改迭代中的集合
for item in list:
list.remove(item) # 导致不可预测行为
八、经典问题解决方案对比
问题类型 | 贪心解法 | 最佳替代方案 |
---|---|---|
最小生成树 | Prim/Kruskal (✓) | - |
最短路径 | Dijkstra (无负权✓) | Bellman-Ford |
背包问题 | 分数背包 (✓) | 动态规划 (0-1背包) |
活动选择 | 最早结束 (✓) | - |
硬币找零 | 特定面值 (✓) | 动态规划 (通用) |
九、贪心算法完整示例:找零钱问题
问题描述
给定不同面额的硬币(如 [1, 5, 10, 25]
美分)和一个总金额(如 36
美分),如何用最少数量的硬币凑出该金额?
- Mermaid 流程图
- Python 代码实现
def greedy_coin_change(coins, amount):
coins.sort(reverse=True) # 降序排序
result = []
remaining = amount
for coin in coins:
if remaining <= 0:
break
count = remaining // coin # 当前硬币最多能用几个
if count > 0:
result.extend([coin] * count)
remaining -= coin * count
return result if remaining == 0 else "无解"
# 示例
coins = [25, 10, 5, 1]
amount = 36
print(greedy_coin_change(coins, amount)) # 输出: [25, 10, 1]
- 算法步骤解析
排序硬币:
[25, 10, 5, 1]
(贪心选择:优先用大面额)迭代处理:
36 ÷ 25 = 1 → 用1个25美分,剩余11
11 ÷ 10 = 1 → 用1个10美分,剩余1
1 ÷ 5 = 0 → 跳过
1 ÷ 1 = 1 → 用1个1美分,剩余0
结果:
[25, 10, 1]
(共3枚硬币)
- 贪心算法的特点
- 优点:简单高效(时间复杂度 O(n))。
- 局限性:不保证全局最优(如硬币为
[4, 3, 1]
,金额6时贪心解[4, 1, 1]
非最优)。 - 适用场景:问题具有贪心选择性质(如硬币面额是倍数关系)。
- 对比动态规划
方法 | 时间复杂度 | 是否全局最优 | 适用场景 |
---|---|---|---|
贪心算法 | O(n) | 不一定 | 问题有贪心性质 |
动态规划 | O(n×amount) | 是 | 通用但计算复杂 |
- 可视化执行过程
金额: 36
1. 选25 → 剩余:11
2. 选10 → 剩余:1
3. 选1 → 剩余:0
结果: [25, 10, 1]