找零钱问题(Change-making Problem)是计算机科学中一个经典的问题,特别是在贪心算法的背景下非常常见。问题可以简单描述为:

给定一个目标金额和若干种不同面额的硬币,如何使用最少数量的硬币组合来凑齐目标金额。

找零钱问题的形式化描述:
  • 输入:一个目标金额 MMM 和一个硬币面额数组 {c1,c2,...,cn}\{c_1, c_2, ..., c_n\}{c1​,c2​,...,cn​},数组中的每个元素代表硬币的面值,面额通常是正整数。
  • 输出:使用的最少硬币数量以及每种面额硬币的数量,使得硬币面值的总和等于目标金额 MMM。

目标:我们需要找出一组硬币组合,使得这些硬币的总金额恰好为 MMM,并且使用的硬币数量尽可能少。

示例:

假设我们有硬币面额:{1,5,10,25}\{1, 5, 10, 25\}{1,5,10,25},需要找零的金额为63。 我们期望得到一种使用最少硬币数的方案,像这样:

  • 2 枚 25分硬币
  • 1 枚 10分硬币
  • 3 枚 1分硬币

总共用6枚硬币构成63。

贪心算法的找零钱问题

贪心算法(Greedy Algorithm)是一种逐步求解问题的策略。每一步都做出当前最优的选择(局部最优解),期望通过这种方式最终得到全局最优解。

在找零钱问题中,贪心算法的核心思想是:

  1. 优先选择面额较大的硬币:尽可能多地使用面额最大的硬币,这样可以在每一步减少目标金额。
  2. 依次尝试较小面额的硬币:在剩余的金额上,继续选择次大面额的硬币,直到总金额达到目标。
贪心算法的步骤:
  1. 降序排列硬币面额:保证从面额最大的硬币开始选择。
  2. 计算使用某种硬币的数量:对于当前面额 cic_ici​,尽量多地使用该硬币,即计算 需要的硬币数量=剩余金额/ci\text{需要的硬币数量} = \text{剩余金额} / c_i需要的硬币数量=剩余金额/ci​。
  3. 更新剩余金额:剩余金额等于当前金额对硬币面额的取余,即 剩余金额=剩余金额%ci\text{剩余金额} = \text{剩余金额} \% c_i剩余金额=剩余金额%ci​。
  4. 重复步骤2和3:依次对较小面额的硬币重复上述过程,直到剩余金额为0。

贪心算法的适用性

虽然贪心算法在许多情况下都能得到最优解,但并不是所有硬币组合下都能使用贪心算法。例如:

  • 当硬币的面额为 {1,3,4}\{1, 3, 4\}{1,3,4} 时,找零金额为 6 的贪心策略会先选择一个面额为4的硬币,剩下的金额是2。这时再选择两个1分硬币,总共用了3枚硬币。
  • 但实际最优解是选择两个3分硬币,总共用2枚硬币。

因此,贪心算法对于某些特定的硬币面额集合可能并不能保证最优解。但对于标准的货币系统(如1元、5元、10元等),贪心策略能提供最优解。

贪心算法 vs 动态规划

对于找零钱问题,**动态规划(Dynamic Programming)**是另一种通用的解法。动态规划通过逐步构建最优子问题的解来获得全局最优解,能够保证在所有硬币组合下找到最优解。它的基本思想是:

  1. 通过递归或自底向上的方式,计算找零金额为 MMM 时最少需要多少硬币。
  2. 通过保存子问题的解,避免重复计算。

动态规划相比贪心算法,适用范围更广,但时间复杂度较高,尤其在问题规模较大的情况下。

贪心算法的优缺点

优点:
  • 简单直观:每次选择当前最优解,易于实现。
  • 效率高:由于不需要考虑全局最优,只需要每次做局部最优选择,因此它的时间复杂度通常较低(例如 O(n)O(n)O(n))。
  • 适用于部分特殊问题:如硬币面额是标准货币系统时,贪心策略能找到最优解。
缺点:
  • 可能得不到全局最优解:贪心算法基于局部最优选择,无法保证全局最优,特别是在面额设计复杂的情况下。
  • 受限于问题结构:贪心算法不是所有问题的通用解法,需要问题具有贪心选择性质,才能使用该方法。

Java代码详细解读

import java.util.Arrays;

public class GreedyChange {

    // 贪心算法实现找零钱问题
    public static void findChange(int[] coins, int amount) {
        // 对硬币面额进行升序排序
        Arrays.sort(coins);  // Arrays.sort会将数组从小到大排序
        // 由于我们要从大面额开始,后面会逆序遍历
        int[] result = new int[coins.length]; // 结果数组,用来记录每种面额使用的硬币数量
        
        // 逆序遍历硬币面额,从最大面额的硬币开始
        for (int i = coins.length - 1; i >= 0; i--) {
            // 如果当前金额大于等于当前硬币面额,则使用该面额硬币
            if (amount >= coins[i]) {
                result[i] = amount / coins[i]; // 计算当前面额硬币的数量
                amount = amount % coins[i];    // 更新剩余金额
            }
        }
        
        // 输出结果:展示每种面额的硬币使用数量
        System.out.println("所需硬币的最少数量是:");
        for (int i = coins.length - 1; i >= 0; i--) {
            if (result[i] != 0) {
                System.out.println("面额 " + coins[i] + " 的硬币: " + result[i] + " 枚");
            }
        }
        
        // 如果还有剩余金额未能找零,说明无法精确找零
        if (amount > 0) {
            System.out.println("无法精确找零,剩余金额: " + amount);
        }
    }

    public static void main(String[] args) {
        // 定义硬币面额数组
        int[] coins = {1, 5, 10, 25}; // 硬币面额
        // 定义需要找零的金额
        int amount = 63; // 需要找零的金额
        // 调用找零钱方法
        findChange(coins, amount); 
    }
}
代码详细解读:
  1. Arrays.sort(coins):对硬币面额数组进行排序,以确保我们可以从最大面额硬币开始选择。由于 Arrays.sort() 默认是升序排序,所以我们在遍历时使用逆序(从最大到最小)。

  2. result[i] = amount / coins[i]:这是计算当前剩余金额可以使用多少枚面额为 coins[i] 的硬币。这个值直接表示我们可以使用多少个该面额的硬币。

  3. amount = amount % coins[i]:计算剩余需要找零的金额。通过模运算(%),我们能够得到在使用该面额硬币之后,剩余的找零金额。

  4. if (result[i] != 0):该条件用于过滤掉那些没有使用的面额硬币,只输出使用到的硬币。

  5. if (amount > 0):在结束硬币选择之后,如果金额还大于0,意味着无法完全找零,比如在某些情况下没有1分硬币的情况下,可能会有无法精确找零的情况。

结论

通过贪心算法解决找零钱问题,我们能够快速获得一个使用最少硬币的解,适合大部分标准货币系统。在实际应用中,可以根据硬币面额系统和问题的具体特点,选择贪心算法或动态规划等更加普适的算法。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐