动态规划vs贪心算法🤔💡

466 阅读6分钟

引言

在之前的文章(递归vs动态规划)中,我们简单介绍了动态规划。这篇文章将继续以力扣的一道算法题为例,探讨贪心算法的基本思想,并与动态规划进行对比。


零钱兑换

LeetCode题库第322题:零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。 你可以认为每种硬币的数量是无限的。


解法之贪心算法

什么是贪心算法

贪心算法(Greedy Algorithm)是一种在每一步都选择当前条件下最优解的策略,期望通过一系列局部最优解达到全局最优解。它简单直观,适用于特定结构的问题。

贪心算法的特点

  • 核心思想:每一步都选择当前最优解,而不考虑未来的影响。
  • 局限性:当问题具有复杂的依赖关系时,贪心算法可能无法保证全局最优解。

解题思路

  1. 初始化:假设 coins 数组是升序排列,定义计数器 count 记录硬币总数。
  2. 寻找最大面值硬币:从最大的硬币面值开始减少总金额。如果当前硬币面值大于剩余金额,则尝试较小面值的硬币。
  3. 更新总金额:成功使用硬币后,更新剩余金额并增加 count
  4. 终止条件:若总金额为 0,返回硬币总数;若无解,返回 -1

代码实现:

function coinChange(coins,amt){
    // 假设coins是升序的
    let i = coins.length - 1;
    let count = 0;
    while(amt>0){ //还要找零
        while(i>0&&coins[i]>amt){
            i--;
        }
        amt -= coins[i];//amt = amt - coins[i]
        count++;
    }

    return amt ===0 ? count : -1;
}
//某种组合是适合贪心的
coinChangeGreedy([1,5,10,20,100],131)//4
// 不适合贪心策略
//coinChangeGreedy([1,20,50],60)//11

这段代码首先从最大的硬币开始尝试减少总额。如果当前最大面值的硬币不能使用(即大于剩余金额),则尝试下一个较小面值的硬币。这个过程持续进行,直到总额为零或者没有合适的硬币可用。

上述代码中,当硬币面值设计较特殊时(如 [1, 20, 50]),贪心算法可能无法得到最优解(如目标金额为 60 时)。此时,可以尝试动态规划。


解法之动态规划

动态规划的核心思想

动态规划通过将问题分解为子问题,并保存其解以避免重复计算。它可以在复杂依赖关系下找到全局最优解。

解题思路

  1. 定义状态:用 f[i] 表示凑成金额 i 的最少硬币个数。
  2. 状态转移方程
  3. 初始化f[0] = 0,表示金额为 0 时不需要任何硬币。
  4. 求解:遍历金额区间 [1, amount] 和硬币数组 coins,逐步填充 f
  5. 返回结果:若 f[amount] 为无穷大,说明无解;否则返回 f[amount]

代码实现:

const coinChange = function(coins, amount) {
    // 用于保存每个目标总额对应的最小硬币个数
    const f = []  //dp数组  初始化
    // 提前定义已知情况
    f[0] = 0
    // 遍历 [1, amount] 这个区间的硬币总额
    for(let i=1;i<=amount;i++) {
        // 求的是最小值,因此我们预设为无穷大,确保它一定会被更小的数更新
        f[i] = Infinity
        // 循环遍历每个可用硬币的面额
        for(let j=0;j<coins.length;j++) {
            // 若硬币面额小于目标总额,则问题成立
            if(i-coins[j]>=0) {
                // 状态转移方程
                f[i] = Math.min(f[i],f[i-coins[j]]+1)
            }
        }
    }
    // 若目标总额对应的解为无穷大,则意味着没有一个符合条件的硬币总数来更新它,本题无解,返回-1
    if(f[amount]===Infinity) {
        return -1
    }
    // 若有解,直接返回解的内容
    return f[amount]
  };

动态规划能够在复杂依赖关系或非标准硬币面值组合的情况下,找到全局最优解。详细解法及思路请看之前的文章: 递归vs动态规划


贪心算法VS动态规划

核心思路对比

  • 贪心算法:在每一步选择当前局部最优的选项,寄希望于通过一系列局部最优解构建全局最优解。
  • 动态规划:通过将问题分解为多个重叠子问题,利用已计算的子问题结果来推导更大问题的解。

性能对比

时间复杂度:

  • 贪心算法:通常是O(n)或O(n log n),取决于选择过程中是否需要排序。一次遍历或简单操作即可解决问题。
  • 动态规划:通常是O(n * m)或更高,其中n是问题规模(如硬币种类数),m是解空间规模(如金额)。

空间复杂度:

  • 贪心算法:只需几个变量记录当前状态,空间复杂度为O(1)。
  • 动态规划:需要存储中间状态,通常为O(n)或O(n * m)。

适用场景

  • 贪心算法:

    1. 问题具有贪心选择性质最优子结构
    2. 局部最优选择必然导致全局最优解。
    3. 更追求效率而非严格的全局最优解)。
  • 动态规划:

    1. 问题存在重叠子问题最优子结构
    2. 需要对所有可能的子问题进行全面分析。
    3. 求解全局最优解,而局部贪心无法满足时。

例子对比

  • 贪心算法:从大到小选择硬币面值。例如,要兑换金额63,先选择最大面值的硬币直到无法继续,再选择次大面值。

    • 优点:高效。
    • 缺点:不一定能保证全局最优(如面值组合[1, 3, 4]换6,贪心选4+1+1=3枚,不是最优解2枚)。
  • 动态规划:枚举所有可能的硬币组合,寻找使用硬币数量最少的方案。

    • 优点:能找到全局最优解。
    • 缺点:耗时较长,空间复杂度较高。

结语

本文探讨了贪心算法和动态规划在零钱兑换问题中的应用与对比。贪心算法简单高效,但局限于特定条件;动态规划适应性更广,但计算成本更高。面对实际问题时,选择合适的算法至关重要。