左哥算法 - 贪心算法

219 阅读5分钟

贪心算法的基本知识

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. 注意事项
  1. 使用前需要证明贪心策略的正确性
  2. 考虑是否存在反例
  3. 注意贪心策略可能带来的局限性
  4. 某些情况下可能需要回溯或动态规划
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. 常见的适合贪心的问题特征
  1. 排序后具有单调性
// 区间调度问题
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
        }
}
  1. 可以进行局部替换
// Huffman编码
class HuffmanNode(
    val char: Char? = null,
    val freq: Int,
    var left: HuffmanNode? = null,
    var right: HuffmanNode? = null
)
  1. 具有贪心选择性质的证明
// 最小生成树的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. 总结建议
  1. 先尝试简单情况

    • 用小规模数据测试
    • 验证贪心策略是否合理
  2. 寻找反例

    • 构造特殊测试用例
    • 验证算法在各种情况下的正确性
  3. 比较其他算法

    • 与动态规划对比
    • 考虑时间和空间复杂度
  4. 证明正确性

    • 使用数学归纳法
    • 使用反证法
    • 使用交换论证

如果问题满足以上特征并通过验证,那么很可能适合使用贪心算法解决。但要注意,即使问题看起来适合贪心,也要谨慎验证其正确性。