引言
在之前的文章(递归vs动态规划)中,我们简单介绍了动态规划。这篇文章将继续以力扣的一道算法题为例,探讨贪心算法的基本思想,并与动态规划进行对比。
零钱兑换
给你一个整数数组coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
解法之贪心算法
什么是贪心算法
贪心算法(Greedy Algorithm)是一种在每一步都选择当前条件下最优解的策略,期望通过一系列局部最优解达到全局最优解。它简单直观,适用于特定结构的问题。
贪心算法的特点
- 核心思想:每一步都选择当前最优解,而不考虑未来的影响。
- 局限性:当问题具有复杂的依赖关系时,贪心算法可能无法保证全局最优解。
解题思路
- 初始化:假设
coins数组是升序排列,定义计数器count记录硬币总数。 - 寻找最大面值硬币:从最大的硬币面值开始减少总金额。如果当前硬币面值大于剩余金额,则尝试较小面值的硬币。
- 更新总金额:成功使用硬币后,更新剩余金额并增加
count。 - 终止条件:若总金额为 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 时)。此时,可以尝试动态规划。
解法之动态规划
动态规划的核心思想
动态规划通过将问题分解为子问题,并保存其解以避免重复计算。它可以在复杂依赖关系下找到全局最优解。
解题思路
- 定义状态:用
f[i]表示凑成金额i的最少硬币个数。 - 状态转移方程:
- 初始化:
f[0] = 0,表示金额为 0 时不需要任何硬币。 - 求解:遍历金额区间
[1, amount]和硬币数组coins,逐步填充f。 - 返回结果:若
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)。
适用场景
-
贪心算法:
- 问题具有贪心选择性质和最优子结构。
- 局部最优选择必然导致全局最优解。
- 更追求效率而非严格的全局最优解)。
-
动态规划:
- 问题存在重叠子问题和最优子结构。
- 需要对所有可能的子问题进行全面分析。
- 求解全局最优解,而局部贪心无法满足时。
例子对比
-
贪心算法:从大到小选择硬币面值。例如,要兑换金额
63,先选择最大面值的硬币直到无法继续,再选择次大面值。- 优点:高效。
- 缺点:不一定能保证全局最优(如面值组合[1, 3, 4]换6,贪心选4+1+1=3枚,不是最优解2枚)。
-
动态规划:枚举所有可能的硬币组合,寻找使用硬币数量最少的方案。
- 优点:能找到全局最优解。
- 缺点:耗时较长,空间复杂度较高。
结语
本文探讨了贪心算法和动态规划在零钱兑换问题中的应用与对比。贪心算法简单高效,但局限于特定条件;动态规划适应性更广,但计算成本更高。面对实际问题时,选择合适的算法至关重要。