返回

硬币找零问题的最优解

前端

在日常生活中,我们经常会遇到硬币找零的问题。想象一下,你在超市购物结账,收银员需要找给你一些零钱,她应该如何选择硬币组合,才能保证使用的硬币数量最少呢?这其实就是一个典型的硬币找零问题。看似简单,但如果要找到最优解,还真需要动一番脑筋。

解决硬币找零问题,计算机科学领域提供了一些非常实用的方法,其中动态规划和贪心算法是两种比较经典的策略。我们先来看看动态规划是如何解决这个问题的。

动态规划有点像我们解决复杂问题时的分治策略,它把一个大问题拆解成许多小的子问题,先解决这些小的子问题,再把它们的解组合起来,最终得到大问题的答案。

具体到硬币找零,我们可以创建一个表格,表格的行代表可用的硬币种类,列代表需要找零的金额。表格中的每个单元格记录着使用对应硬币凑成对应金额所需的最少硬币数。

我们先初始化表格的第一行和第一列,表示不使用任何硬币或找零金额为0的情况。然后,我们按照从左到右,从上到下的顺序依次填写表格的其余单元格。

填写每个单元格时,我们需要考虑两种情况:

  1. 如果当前硬币的面额小于等于需要找零的金额,我们有两个选择:要么使用当前硬币,要么不使用。如果使用,则所需硬币数为表格中上一行对应金额减去当前硬币面额的单元格的值加 1;如果不使用,则所需硬币数为表格中上一行对应金额的单元格的值。我们选择两者中较小的那个。
  2. 如果当前硬币的面额大于需要找零的金额,我们无法使用当前硬币,因此所需硬币数与表格中上一行对应金额的单元格的值相同。

通过这种方式,我们可以逐步填写完整个表格,最终表格右下角的单元格的值就是使用所有可用硬币凑成目标金额所需的最少硬币数。

下面我们用 Python 代码来实现一下动态规划的解法:

def coin_change_dp(coins, amount):
    dp = [[float('inf')] * (amount + 1) for _ in range(len(coins) + 1)]
    
    # 初始化
    for i in range(len(coins) + 1):
        dp[i][0] = 0
    
    # 递推
    for i in range(1, len(coins) + 1):
        for j in range(1, amount + 1):
            if coins[i - 1] <= j:
                dp[i][j] = min(dp[i - 1][j], 1 + dp[i][j - coins[i - 1]])
            else:
                dp[i][j] = dp[i - 1][j]
    
    return dp[len(coins)][amount]

动态规划虽然能够保证找到最优解,但它需要创建一个表格来存储中间结果,因此空间复杂度较高。如果可用硬币种类很多,或者需要找零的金额很大,那么动态规划所需的存储空间可能会非常大。

这时候,我们可以考虑使用另一种算法:贪心算法。贪心算法的思路很简单,它每次都选择当前看来最优的策略,希望通过一系列局部最优的选择最终达到全局最优。

在硬币找零问题中,贪心算法的做法是:每次都选择面额最大的硬币,直到凑够目标金额为止。

例如,如果可用硬币面额为 1 元、2 元和 5 元,需要找零 9 元,那么贪心算法会先选择 2 个 5 元硬币,然后再选择 1 个 1 元硬币,最终使用了 3 个硬币。

下面我们用 Python 代码来实现一下贪心算法的解法:

def coin_change_greedy(coins, amount):
    coins.sort(reverse=True)
    result = []
    
    for coin in coins:
        while amount >= coin:
            amount -= coin
            result.append(coin)
    
    return len(result)

贪心算法的优点是简单易懂,且时间复杂度较低。但它并不总是能找到最优解。例如,如果可用硬币面额为 1 元、3 元和 4 元,需要找零 6 元,那么贪心算法会先选择 1 个 4 元硬币,然后再选择 2 个 1 元硬币,最终使用了 3 个硬币。而实际上,最优解是选择 2 个 3 元硬币,只需要使用 2 个硬币。

那么,我们应该如何选择使用哪种算法呢?

一般来说,如果可用硬币的面额满足一定的条件(例如,每个硬币的面额都是前一个硬币面额的倍数),那么贪心算法就能找到最优解。否则,我们最好使用动态规划来保证找到最优解。

常见问题解答

1. 动态规划和贪心算法有什么区别?

动态规划是一种将问题分解成子问题,并存储子问题的解来避免重复计算的算法。贪心算法是一种每次都选择当前看来最优的策略的算法。动态规划通常可以找到最优解,但空间复杂度较高;贪心算法简单易懂,时间复杂度较低,但并不总是能找到最优解。

2. 硬币找零问题可以用其他算法解决吗?

除了动态规划和贪心算法,硬币找零问题还可以用回溯法、分支限界法等算法解决。但这些算法的时间复杂度通常较高,不太适合解决大规模问题。

3. 如何判断贪心算法是否能找到硬币找零问题的最优解?

如果可用硬币的面额满足一定的条件(例如,每个硬币的面额都是前一个硬币面额的倍数),那么贪心算法就能找到最优解。否则,贪心算法可能无法找到最优解。

4. 如何优化动态规划算法的空间复杂度?

可以使用滚动数组来优化动态规划算法的空间复杂度。滚动数组的思想是,在计算当前行的值时,只需要用到上一行的值,因此只需要存储两行的值即可,不需要存储整个表格。

5. 硬币找零问题在实际生活中有哪些应用?

硬币找零问题在自动售货机、收银系统等场景中都有应用。例如,自动售货机需要根据用户投入的金额和商品价格计算找零金额,并选择合适的硬币组合找零。