Dynamic Programming学习笔记 (13) - 零钱兑换二 (力扣 #518)

400 阅读2分钟

零钱兑换问题是DP学习入门阶段必须掌握的一个经典应用,其题面如下:

给定一个整数数组 coins 表示N种不同面额的硬币,另给一个整数 amount 表示总金额。 计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。 假设每一种面额的硬币有无限个。

实例:

输入:amount = 5, coins = {1, 2, 5}
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

解题思路如下:

出发点在于定义一个函数F(k,m),k是[0, amount]之间的各个整数,m是[0, N]之间的各个整数,F(k,m)的返回值代表使用前m种硬币可以凑成金额k的组合数。

依循DP原理,我们可以将F(k,m)所代表的所有组合分成两个部分。

第一部分是不包含第m种硬币的组合数,等于 F(k, m - 1),第二部分是包含第m种硬币的组合数,等于 F(k - coins[m], m)

由此我们可以等到

F(k,m)
     =  F(k, m - 1), k < coins[m] 
     =  F(k, m - 1) + F(k - coins[m], m), k >= coins[m] 

边界值:
k = 0,返回1,代表当金额为0时,存在一种组合,也就是一个硬币也不用
k > 0, m = 0,返回0,代表当金额大于0,但没有可用的硬币时,不存在任何可能的组合

F(k,m)有两个参数,因此我们可以使用二维数组作为DP存储,并使用双重循环根据k和m的数值从小到大依次计算DP数组中的各个元素,最后DP[amount][N]中的数值就是问题的答案。

Java代码如下:

class Solution {
    public int change(int amount, int[] coins) {
        if(amount == 0) {
            return 1;
        }

        int N = coins.length;

        int[][] dp = new int[amount + 1][N + 1];

        for (int m = 0; m <= N; m ++) {
            dp[0][m] = 1;
        }

        for (int k = 1; k <= amount; k ++) {
            for (int m = 1; m <= N; m ++) {
                dp[k][m] = dp[k][m - 1];

                int coin = coins[m - 1];
                if (k >= coin) {
                    dp[k][m] += dp[k - coin][m];
                }
            }
        }

        return dp[amount][N];
    }
}

以上算法可以进一步优化使用一维的DP数组。