贪心算法的基本知识
1. 什么是贪心算法?
贪心算法就像"只看眼前利益"的决策方式:
- 每一步都选择当前看起来最优的选择
- 不考虑这个选择对未来的影响
- 期望通过局部最优达到全局最优
2. 生动示例
想象你在玩一个找零钱的游戏:
需要找零 98 元,手里有 50、20、10、5、1 元的钞票
贪心策略:
1. 先用 50 元 (因为最大) → 还需要 48 元
2. 再用 20 元 (当前最大) → 还需要 28 元
3. 再用 20 元 → 还需要 8 元
4. 再用 5 元 → 还需要 3 元
5. 最后用 3 个 1 元
最终方案:50 + 20 + 20 + 5 + 1 + 1 + 1 = 98
3. 贪心算法的特点
优点:
1. 速度快
2. 实现简单
3. 容易理解
缺点:
1. 不一定能得到最优解
2. 依赖问题特性
3. 需要证明贪心选择的正确性
4. 适用场景
// 1. 活动选择问题
data class Activity(val start: Int, val end: Int)
fun selectActivities(activities: List<Activity>): List<Activity> {
// 按结束时间排序
val sorted = activities.sortedBy { it.end }
val result = mutableListOf<Activity>()
var lastEnd = 0
for (activity in sorted) {
if (activity.start >= lastEnd) {
result.add(activity)
lastEnd = activity.end
}
}
return result
}
// 2. Huffman编码
// 3. 最小生成树的Prim算法
// 4. 找零钱问题
5. 贪心算法的一般步骤
1. 定义问题的解空间
2. 确定局部最优策略
3. 按策略贪心选择
4. 判断是否达到最终解
6. 流程图
graph TD
A[开始] --> B[定义问题解空间]
B --> C[确定贪心策略]
C --> D[初始化解集合]
D --> E{是否达到最终解?}
E -- 否 --> F[根据贪心策略选择当前最优解]
F --> G[将选择加入解集合]
G --> E
E -- 是 --> H[输出最终解]
H --> I[结束]
7. 代码模板
fun greedyAlgorithm(problem: Problem): Solution {
// 1. 初始化解集合
val solution = mutableListOf<Choice>()
// 2. 当问题还未解决时
while (!problem.isSolved()) {
// 3. 获取当前所有可行选择
val choices = problem.getValidChoices()
// 4. 根据贪心策略选择当前最优解
val bestChoice = choices.maxBy { it.value }
// 5. 做出选择
solution.add(bestChoice)
problem.makeChoice(bestChoice)
}
return Solution(solution)
}
8. 实际应用示例
// 分发饼干问题
fun findContentChildren(g: IntArray, s: IntArray): Int {
// 胃口值和饼干尺寸排序
g.sort()
s.sort()
var child = 0
var cookie = 0
// 贪心策略:用尽可能小的饼干满足胃口小的孩子
while (child < g.size && cookie < s.size) {
if (s[cookie] >= g[child]) {
child++
}
cookie++
}
return child
}
9. 注意事项
- 使用前需要证明贪心策略的正确性
- 考虑是否存在反例
- 注意贪心策略可能带来的局限性
- 某些情况下可能需要回溯或动态规划
10. 总结
贪心算法是一种通过局部最优选择,期望达到全局最优的算法策略。它简单高效,但需要仔细设计贪心策略并证明其正确性。在某些特定问题上,贪心算法可以得到最优解,但并不是所有问题都适用。
如何判断问题可以使用贪心解决
1. 判断问题是否具有贪心选择性质
贪心选择性质的特点:
1. 当前选择不会影响未来选择
2. 当前选择只与当前状态有关
3. 局部最优可以导致全局最优
示例分析:
// 适合贪心:找零钱问题
fun makeChange(amount: Int, coins: IntArray): Int {
coins.sortDescending() // 从大到小排序
var remaining = amount
var count = 0
for (coin in coins) {
while (remaining >= coin) {
remaining -= coin
count++
}
}
return count
}
// 不适合贪心:0-1背包问题
// 因为选择当前物品会影响后续物品的选择空间
2. 检查问题是否满足最优子结构
最优子结构特点:
1. 问题的最优解包含子问题的最优解
2. 子问题之间相互独立
判断流程图:
graph TD
A[问题] --> B{是否有子问题?}
B -- 是 --> C{子问题是否独立?}
B -- 否 --> D[可能适合贪心]
C -- 是 --> E{局部最优是否导致全局最优?}
C -- 否 --> F[不适合贪心]
E -- 是 --> G[适合贪心]
E -- 否 --> F
3. 尝试反证法
步骤:
1. 假设存在比贪心解更优的解
2. 尝试证明这个解可以转换成贪心解
3. 如果能证明,则贪心正确
示例:
// 活动选择问题的反证
fun proveActivitySelection() {
/*
假设: 存在最优解S*不同于贪心解S
反证步骤:
1. 找到S*和S第一个不同的活动
2. 证明可以用S中的活动替换S*中的活动
3. 替换后解仍然最优
*/
}
4. 常见的适合贪心的问题特征
- 排序后具有单调性
// 区间调度问题
data class Interval(val start: Int, val end: Int)
fun maxIntervals(intervals: List<Interval>): List<Interval> {
// 按结束时间排序后具有单调性
return intervals.sortedBy { it.end }
.fold(mutableListOf()) { selected, interval ->
if (selected.isEmpty() || selected.last().end <= interval.start) {
selected.add(interval)
}
selected
}
}
- 可以进行局部替换
// Huffman编码
class HuffmanNode(
val char: Char? = null,
val freq: Int,
var left: HuffmanNode? = null,
var right: HuffmanNode? = null
)
- 具有贪心选择性质的证明
// 最小生成树的Prim算法
fun primMST(graph: Array<IntArray>): List<Edge> {
/*
贪心选择性质证明:
1. 每次选择最小权重的边
2. 证明这种选择不会影响最终结果
3. 利用切分定理证明正确性
*/
}
5. 实际判断步骤
graph TD
A[新问题] --> B{是否具有贪心选择性质?}
B -- 是 --> C{是否具有最优子结构?}
B -- 否 --> D[考虑其他算法]
C -- 是 --> E{能否证明贪心正确性?}
C -- 否 --> D
E -- 是 --> F[使用贪心算法]
E -- 否 --> G{是否有反例?}
G -- 有 --> D
G -- 没有 --> H[需要进一步分析]
6. 验证示例
// 判断问题是否适合贪心的检查表
fun checkGreedyApplicability(problem: Problem): Boolean {
// 1. 检查贪心选择性质
if (!hasGreedyChoiceProperty(problem)) return false
// 2. 检查最优子结构
if (!hasOptimalSubstructure(problem)) return false
// 3. 尝试构造反例
if (hasCounterExample(problem)) return false
// 4. 验证局部最优到全局最优
if (!canProveLocalToGlobal(problem)) return false
return true
}
7. 总结建议
-
先尝试简单情况
- 用小规模数据测试
- 验证贪心策略是否合理
-
寻找反例
- 构造特殊测试用例
- 验证算法在各种情况下的正确性
-
比较其他算法
- 与动态规划对比
- 考虑时间和空间复杂度
-
证明正确性
- 使用数学归纳法
- 使用反证法
- 使用交换论证
如果问题满足以上特征并通过验证,那么很可能适合使用贪心算法解决。但要注意,即使问题看起来适合贪心,也要谨慎验证其正确性。