ACM 算法入门 · 贪心篇:从局部最优到整体最优的策略思维
ACM 算法入门 · 贪心篇:从局部最优到整体最优的策略思维
什么是贪心算法?
贪心算法(Greedy Algorithm) 是一种在每一步选择中都采取当前状态下最优(即最有利)选择的策略,期望通过局部最优最终达到全局最优。
它的核心思想是:
局部最优 ➜ 全局最优
但需要特别注意的是:贪心策略并不总是适用于所有问题,它只适合满足以下两个条件的问题:
- ✅ 最优子结构:问题的最优解包含其子问题的最优解;
- ✅ 无后效性:某一步的选择不会影响后续的选择过程。
如果问题不满足这两个性质,使用贪心算法可能得不到正确解。
贪心算法的基本思路
贪心算法虽然没有统一的模板,但通常遵循以下解决步骤:
- 建立数学模型:用合理的数据结构和变量表达问题;
- 拆解为子问题:将原问题划分为一系列阶段性子问题;
- 定义贪心策略:找出每一步的“最优选择”规则;
- 按策略求解子问题:每次都按照贪心策略做选择;
- 组合局部最优解:最终汇总各步的选择,构成整体解。
贪心算法存在的问题
- 不能保证最终求得的解一定是全局最优;
- 不适用于所有需要求最大值或最小值的问题;
- 只能求满足某些特定约束条件下的可行解。
贪心算法适用的问题
贪心算法适用的前提是:局部最优策略能够导致全局最优解。
实际上,适用场景较少。判断一个问题是否适合贪心算法,通常可以通过实际数据测试和理论分析来确定。
贪心选择性质
贪心选择性质:问题的整体最优解可通过一系列局部最优选择构建。
换言之,在做每一步选择时,只关注当前问题的最优选择,不考虑后续子问题的解。贪心算法通过迭代进行,每次选择都会简化问题规模,形成更小的子问题。
最优子结构性质:问题的最优解包含其子问题的最优解。
这是贪心算法能正确求解问题的关键。
贪心算法的实现框架
1 | 从初始解开始: |
✨ 示例:找零钱问题
目标:用尽量少的硬币凑出某个金额
硬币面额:25 分、10 分、5 分、1 分
贪心策略:每次选择当前面值最大的硬币
1 | def greedy_coin_change(amount): |
贪心策略:每次优先选面值最大的硬币。
选择步骤:
63 分 ——> 选择 2 个 25 分硬币,剩余 63 - 50 = 13 分
13 分 ——> 选择 1 个 10 分硬币,剩余 13 - 10 = 3 分
3 分 ——> 选择 0 个 5 分硬币(3 分不足 5 分),剩余 3 分
3 分 ——> 选择 3 个 1 分硬币,剩余 3 - 3 = 0 分,完成!
硬币组合:25 分 × 2 + 10 分 × 1 + 1 分 × 3 = 63 分
1 | [63元] |
贪心算法经典算法题
背包问题
题目:
有一个背包,容量为 M = 150,有 7 个物品,物品可分割成任意大小。
要求尽可能使装入背包的物品总价值最大,但不能超过总容量。
物品 | A | B | C | D | E | F | G |
---|---|---|---|---|---|---|---|
重量 | 35 | 30 | 60 | 50 | 40 | 10 | 25 |
价值 | 10 | 40 | 30 | 50 | 35 | 40 | 30 |
目标与约束
- 目标函数:最大化总价值 ( \sum p_i )
- 约束条件:总重量不超过背包容量
贪心策略分析
针对该问题,考虑三种贪心策略:
- 每次选择价值最高的物品装入背包
- 每次选择重量最小的物品装入背包
- 每次选择单位重量价值最高的物品装入背包
策略有效性探讨
策略 1:选择价值最大物品
不一定最优。
反例:- 容量 ( W=30 )
- 物品:A(28kg,30 价), B(12kg,20 价), C(12kg,20 价)
该策略先选 A,背包剩余空间不足以选 B 和 C。但选 B 和 C 总价值 40,优于选 A 的 30。
策略 2:选择重量最小物品
同样不一定最优,类似策略 1 的反例适用。策略 3:选择单位重量价值最高物品
是解决分数背包的经典策略,通常最优。
但若单位价值相同,则需结合次级规则(如优先选重量小的物品)来避免歧义。
1 | # 背包问题的贪心算法实现(分数背包) |
分发糖果(Leetcode 题目)
题目:
老师想给站成一排的 N 个孩子分发糖果,分配规则如下:
- 每个孩子至少分配到 1 个糖果;
- 相邻的孩子中,评分更高的孩子必须获得更多的糖果。
那么,老师至少需要准备多少颗糖果呢?
题目示例
示例 1:
输入:
[1,0,2]
输出:5
解释:可以分配糖果为[2,1,2]
。示例 2:
输入:
[1,2,2]
输出:4
解释:可以分配糖果为[1,2,1]
,第三个孩子得到 1 颗糖果,满足条件。
规则分析
设相邻的两个孩子为 A 和 B,A 在 B 左边,则:
- 左规则:当
ratings[B] > ratings[A]
时,B 得到的糖果数比 A 多; - 右规则:当
ratings[A] > ratings[B]
时,A 得到的糖果数比 B 多。
要满足所有相邻孩子都符合这两个规则,意味着每个孩子的糖果数既满足左规则又满足右规则。
算法思路
初始化:先给每个孩子 1 颗糖果。
从左到右遍历,根据左规则更新糖果数:
- 若当前孩子评分比左边孩子高,则当前孩子的糖果数为左边孩子糖果数 + 1;
- 否则保持不变。
从右到左遍历,根据右规则更新糖果数:
- 若当前孩子评分比右边孩子高,且糖果数不够,则更新为右边孩子糖果数 + 1。
最后,所有孩子糖果数取两次遍历后的最大值即满足左右规则,计算总和即为所需糖果总数。
代码实现(C++)
1 | class Solution { |