零钱兑换问题(非常详细,附带源码)

 
零钱兑换问题又叫凑零钱问题,给定不同面值、数量不限的纸币,要求计算拼凑指定的金额最少需要多少张纸币。

举个简单的例子,有 1、7、10 这 3 种面值的纸币,每种纸币的数量不限,要求计算拼凑总面值 15 最少需要多少张纸币。拼凑出总面值 15 的方案有 4 种,分别是:
  • 15 张面值为 1 的纸币;
  • 1 张面值为 7 的纸币和 8 张面值为 1 的纸币,一共需要 9 张;
  • 2 张面值为 7 的纸币和 1 张面值为 1 的纸币,一共需要 3 张;
  • 1 张面值为 10 的纸币和 5 张面值为 1 的纸币,一共需要 6 张。

其中,第 3 个方案(7+7+1)拼凑的总面值是 15,并且使用的纸币数量最少。

零钱兑换问题可以用动态规划算法解决,接下来给大家详细讲解具体的实现思路。

动态规划解决零钱兑换问题

接下来仍以“有 1、7、10 这 3 种面值的纸币,计算拼凑总面值 15 最少需要多少张纸币”为例,带大家梳理动态规划思想解决此类问题的过程。

假设用 f(n) 表示凑齐面值 n 所需纸币的最少数量,面值 15 的拼凑方案有 3 种,分别是:
  • f(15) = f(14) +1:选择一张面值为 1 的纸币,f(14) 表示拼凑出面值 14 所需要的最少的纸币数量;
  • f(15) = f(8) + 1:选择一张面值为 7 的纸币,f(8) 表示拼凑出面值 8 所需要的最少的纸币数量;
  • f(15) = f(5) + 1:选择一张面值为10 的纸币,f(5) 表示拼凑出面值 5 所需要的最少的纸币数量。

以上 3 种选择方案中,所需纸张数量最少的就是最优的拼凑方案。而想要比较它们,需要先求出 f(14)、f(8)、f(5) 的值。

f(5) = f(4)+1:选择一张面值为 1 的纸币,再加上 f(4) 的值。

f(8) = f(7) + 1:选择一张面值为 1 的纸币,再加上 f(7) 的值。
f(8) = f(1) +1:选择一张面值为 7 的纸币,再加上 f(1) 的值。

f(14) = f(13)+1:选择一张面值为 1 的纸币,再加上 f(13) 的值。
f(14) = f(7) + 1:选择一张面值为 7 的纸币,再加上 f(7) 的值。
f(14) = f(4) +1:选择一张面值为 10 的纸币,再加上 f(4) 的值。

f(5) 的拼凑方案只有 1 种,和 f(4) 有关联;f(8) 的拼凑方案有 2 种,分别和 f(7)、f(1) 有关联;f(14) 的拼凑方案有 3 种,分别和 f(13)、f(7)、f(4) 有关联。

f(4)、f(7)、f(13) 还可以继续拆分,它们最终会和 f(0)、f(1)、f(2) 产生关联,而 f(0)、f(1)、f(2) 的值是很容易计算的。在得知 f(0)、f(1)、f(2) 等简单问题的结果后,就可以轻松推算出 f(3)~f(14) 的值,进而推算出 f(15) 的值。

因此,零钱兑换问题的解决思路是:从 f(0) 开始,依次计算 f(1)、f(2)......f(n-1)、f(n) 的值,最终的 f(n) 就是凑齐面值 n 所需纸币的最少数量。

计算 f(0) ~ f(n),它们每一个的值都和 3 种面值纸币的取舍有关系。为了方便读者理解,特意制作了下面的表格:

表1:不选择任何纸币
选择的纸币 f(n) 的所有情况
f(0) f(1) f(2) f(3) f(4) f(5) f(6) f(7) f(8) f(9) f(10) f(11) f(12) f(13) f(14) f(15)
不选择任何纸币 0

在不选择任何纸币的情况下,f(1)~f(15) 的值用 ∞(无穷大)表示,f(0) 的值是 0。

1) 考虑选择面值为 1 的纸币,f(1)~f(15) 的计算过程是:
  • f(1):选择一张面值为 1 的纸币,那么 f(1) 的值可以用 1+f(0) 表示,1 指的是一张面值为 1 的纸币,f(0) 指的是拼凑金额 0 所需纸币的最少数量。1+f(0) = 1,比 f(1) 的当前值 ∞ 小,表明 1+f(0) 的拼凑方案更佳;
  • f(2): 选择一张面值为 1 的纸币,那么 f(2) 的值可以用 1+f(1) 表示。1+f(1) = 2,比 f(2) 的当前值 ∞ 小,表明 1+f(1) 的拼凑方案更佳;
  • f(3): 选择一张面值为 1 的纸币,那么 f(3) 的值可以用 1+f(2) 表示。1+f(2) = 3,比 f(3) 的当前值 ∞ 小,表明 1+f(2) 的拼凑方案更佳;
  • ......
  • f(15):选择一张面值为 1 的纸币,那么 f(15) 的值可以用 1+f(14) 表示。1+f(14) = 15,比 f(15) 的当前值 ∞ 小,表明 1+f(14) 的拼凑方案更佳。

更新表 1 中各个拼凑方案对应的值,如表 2 所示:

表2:选择面值为 1 的纸币
选择的纸币 f(n) 的所有情况
f(0) f(1) f(2) f(3) f(4) f(5) f(6) f(7) f(8) f(9) f(10) f(11) f(12) f(13) f(14) f(15)
面值为 1 的纸币 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

2) 考虑选择面值为 7 的纸币,f(1)~f(15) 的计算过程是:
  • f(1)~f(6):无法选择面值为 7 个纸币;
  • f(7): 选择一张面值为 7 的纸币,那么 f(7) 的值可以用 1+f(0) 表示,1 指的是一张面值为 7 的纸币,f(0) 指的是拼凑金额 0 所需纸币的最少数量。1+f(0) = 1,比 f(7) 的当前值 7 小,表明 1+f(0) 的拼凑方案更佳;
  • f(8): 选择一张面值为 7 的纸币,那么 f(8) 的值可以用 1+f(1) 表示。1+f(1) = 2,比 f(8) 的当前值 8 小,表明 1+f(1) 的拼凑方案更佳;
  • f(9): 选择一张面值为 7 的纸币,那么 f(9) 的值可以用 1+f(2) 表示。1+f(2) = 3,比 f(9) 的当前值 9 小,表明 1+f(2) 的拼凑方案更佳;
  • ......
  • f(15):选择一张面值为 7 的纸币,那么 f(15) 的值可以用 1+f(8) 表示。1+f(8) = 3,比 f(15) 的当前值 15 小,表明 1+f(8) 的拼凑方案更佳。

更新表 2 中 f(7)~f(15) 对应的值,如表 3 所示:

表3:选择面值为 7 的纸币
选择的纸币 f(n) 的所有情况
f(0) f(1) f(2) f(3) f(4) f(5) f(6) f(7) f(8) f(9) f(10) f(11) f(12) f(13) f(14) f(15)
面值为 7 的纸币 0 1 2 3 4 5 6 1 2 3 4 5 6 7 2 3


3) 考虑选择面值为 10 的纸币,f(1)~f(15) 的计算过程是:
  • f(1)~f(9):无法选择面值为 10 个纸币;
  • f(10): 选择一张面值为 10 的纸币,那么 f(10) 的值可以用 1+f(0) 表示,1 指的是一张面值为 10 的纸币,f(0) 指的是拼凑金额 0 所需纸币的最少数量。1+f(0) = 1,比 f(10) 的当前值 4 小,表明 1+f(0) 的拼凑方案更佳;
  • f(11): 选择一张面值为 10 的纸币,那么 f(11) 的值可以用 1+f(1) 表示。1+f(1) = 2,比 f(11) 的当前值 5 小,表明 1+f(1) 的拼凑方案更佳;
  • f(12): 选择一张面值为 10 的纸币,那么 f(12) 的值可以用 1+f(2) 表示。1+f(2) = 3,比 f(12) 的当前值 6 小,表明 1+f(2) 的拼凑方案更佳;
  • f(13):选择一张面值为 10 的纸币,那么 f(13) 的值可以用 1+f(3) 表示。1+f(3) = 4,比 f(13) 的当前值 7 小,表明 1+f(3) 的拼凑方案更佳;
  • f(14):选择一张面值为 10 的纸币,那么 f(14) 的值可以用 1+f(4) 表示。1+f(4) = 5,比 f(14) 的当前值 2 大,表明先前的拼凑方案更佳;
  • f(15):选择一张面值为 10 的纸币,那么 f(15) 的值可以用 1+f(5) 表示。1+f(5) = 6,比 f(15) 的当前值 3 大,表明先前的拼凑方案更佳。

对于 f(14) 和 f(15) 来说,选择面值为 10 的纸币并没有先前的拼凑方案好,所以只更新表 3 中 f(10)~f(13) 对应的值,如表 4 所示:

表4:选择面值为 10 的纸币
选择的纸币 f(n) 的所有情况
f(0) f(1) f(2) f(3) f(4) f(5) f(6) f(7) f(8) f(9) f(10) f(11) f(12) f(13) f(14) f(15)
选择面值为 10 的纸币 0 1 2 3 4 5 6 1 2 3 1 2 3 4 2 3

最终,f(15) 对应的值是 3,表明拼凑出总面值 15 最少需要 3 张纸币。

零钱兑换问题的具体实现

下面是解决零钱兑换问题的伪代码,如果你理解了前面动态规划解决零钱兑换问题的思路,相信能很轻松地理解每行代码的含义。
// coins 存储纸币的不同面值,amount 表示要拼凑的总面值
minCoins(coins, amount):
    n <- length(coins)
   
    // 设置初始条件
    dp[0] <- 0
   
    // 创建一个数组来存储计算过的结果,初始化为无穷大
    dp[1...amount] <- [INT_MAX, INT_MAX, ..., INT_MAX]
   
    // 动态规划计算每个面值的最少纸币数量
    for i <- 0 to n:  // 遍历每个面值
        for j <- coins[i] to amount: // 遍历 f(coins[i])~f(amount) 这些拼凑方案
            if dp[j - coins[i]] != INT_MAX:
                dp[j] = min(dp[j], dp[j - coins[i]] + 1)

    // 返回凑齐面值 amount 所需的最少纸币数量
    return dp[amount]

下面是动态规划解决零钱兑换问题的 C 语言程序:
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

// 计算凑齐面值 n 所需的最少纸币数量
int minCoins(int coins[], int numCoins, int amount){
    // 创建一个数组来存储计算过的结果,初始化为无穷大
    int* dp = (int*)malloc((amount + 1) * sizeof(int));
   
    // 设置初始条件
    dp[0] = 0;
    for (int i = 1; i <= amount; ++i) {
        dp[i] = INT_MAX;
    }

    // 动态规划计算每个面值的最少纸币数量
    for (int i = 0; i < numCoins; ++i) {
        for (int j = coins[i]; j <= amount; ++j) {
            if (dp[j - coins[i]] != INT_MAX) {
                dp[j] = dp[j] < dp[j - coins[i]] + 1 ? dp[j] : dp[j - coins[i]] + 1;
            }
        }

    }
    // 返回凑齐面值 n 所需的最少纸币数量
    return dp[amount];
}

int main() {
    // 定义面值数组和数组长度
    int coins[] = { 1, 7, 10 };
    int numCoins = sizeof(coins) / sizeof(coins[0]);
    int amount = 15;
    int result = minCoins(coins, numCoins, amount);

    if (result != INT_MAX) {
        printf("凑齐面值 %d 最少需要 %d 张纸币\n", amount, result);
    }
    else {
        printf("无法凑齐面值 %d\n", amount);
    }

    return 0;
}

下面是动态规划解决零钱兑换问题的 Java 程序:
public class MinCoins {
    // 计算凑齐面值 n 所需的最少纸币数量
    static int minCoins(int[] coins, int amount) {
        // 创建一个数组来存储计算过的结果,初始化为无穷大
        int[] dp = new int[amount + 1];

        // 设置初始条件
        dp[0] = 0;
        for (int i = 1; i <= amount; ++i) {
            dp[i] = Integer.MAX_VALUE;
        }

        // 动态规划计算每个面值的最少纸币数量
        for (int coin : coins) {
            for (int j = coin; j <= amount; ++j) {
                if (dp[j - coin] != Integer.MAX_VALUE) {
                    dp[j] = Math.min(dp[j], dp[j - coin] + 1);
                }
            }
        }

        // 返回凑齐面值 n 所需的最少纸币数量
        return dp[amount];
    }

    public static void main(String[] args) {
        // 定义面值数组
        int[] coins = {1, 7, 10};
        int amount = 15;
        int result = minCoins(coins, amount);

        if (result != Integer.MAX_VALUE) {
            System.out.printf("凑齐面值 %d 最少需要 %d 张纸币\n", amount, result);
        } else {
            System.out.printf("无法凑齐面值 %d\n", amount);
        }
    }
}

下面是动态规划解决零钱兑换问题的 Python 程序:
def minCoins(coins, amount):
    # 创建一个数组来存储计算过的结果,初始化为无穷大
    dp = [float('inf')] * (amount + 1)

    # 设置初始条件
    dp[0] = 0

    # 动态规划计算每个面值的最少纸币数量
    for coin in coins:
        for j in range(coin, amount + 1):
            if dp[j - coin] != float('inf'):
                dp[j] = min(dp[j], dp[j - coin] + 1)

    # 返回凑齐面值 n 所需的最少纸币数量
    return dp[amount]

if __name__ == "__main__":
    # 定义面值数组
    coins = [1, 7, 10]
    amount = 15
    result = minCoins(coins, amount)

    if result != float('inf'):
        print(f"凑齐面值 {amount} 最少需要 {result} 张纸币")
    else:
        print(f"无法凑齐面值 {amount}")

运行程序,输出结果为:

凑齐面值 15 最少需要 3 张纸币