LeetCode 518.零钱兑换II 动态规划 + 完全背包 + 组合数

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

假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带符号整数。

示例 1:

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

示例 2:

输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3 。

示例 3:

输入:amount = 10, coins = [10] 
输出:1

>>思路和分析

  • ① 钱币数量不限,可以知道这是一个完全背包的问题;
  • ② 与纯完全背包式凑成背包最大价值是多少,而本题是要求凑成总金额的物品组合个数!

注意题目描述中是凑成总金额的硬币组合数,为什么强调是组合数呢?

例如示例一:

        5 = 2 + 2 + 1 

        5 = 2 + 1 + 2

这是一种组合,都是 2 2 1。如果问的是排列数,上面就是两种排列了。

注意:组合不强调元素之间的顺序,排列强调元素之间的顺序

>>动规五部曲

1.确定dp数组以及下标的含义

        dp[j] : 凑成总金额 j 的货币组合数 为 dp[j]

2.确定递推公式

  •  dp[j] 就是所有的 dp[j - coins[i]] (考虑 coins[i] 的情况)相加
  •  所以递推公式:dp[j] += dp[j - coins[i]];

0-1背包题目有这篇LeetCode 494.目标和中讲解了,求装满背包有几种方法,公式都是:

                                                        dp[j] += dp[j - nums[i]];

3.dp数组初始化

dp[0] = 1,这是递归公式的基础;若dp[0] = 0的话,后面所有推导出来的值都是0了

下标非0的dp[j]初始化为0,这样累计加dp[j - coins[i]]的时候才不会影响真正的 dp[j]

dp[0] = 1 还说明了一种情况:如果正好选了coins[i]后,也就是 j - coins[i] == 0的情况表示这个硬币刚好能选,此时 dp[0] 为1 表示只选coins[i]存在这样的一种选法

4.确定遍历顺序

  • 方式一:先遍历物品再遍历背包
  • 方式二:先遍历背包再遍历物品

在纯完全背包中,先遍历物品再遍历背包,还是先遍历背包再遍历物品,两者都可以!

本题就不行了!因为纯完全背包求得装满背包的最大价值是多少,和凑成总和的元素有没有顺序是没有关系的,即:有顺序也行,没顺序也行!

但在本题中要求凑合总和的组合数,元素之间明确要求没有顺序的。所以只能是方式一这种遍历顺序,那为什么不能是方式二呢?

对于方式一在完全背包中:外层for 循环遍历物品(钱币),内层for遍历背包(金钱总额)的情况

for (int i = 0; i < coins.size(); i++) { // 遍历物品
    for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
        dp[j] += dp[j - coins[i]];
    }
}

假设:coins[0] = 1,coins[1] = 5

那么就是先把1加入计算,再把5加入计算,得到的只有{1,5}这种情况,是不会出现{5,1}这种情况的,所以这种遍历顺序中dp[j]里计算的是组合数!

对于方式二在完全背包中:外层for遍历背包(金钱总额),内层for 循环遍历物品(钱币)的情况

for (int j = 0; j <= amount; j++) { // 遍历背包容量
    for (int i = 0; i < coins.size(); i++) { // 遍历物品
        if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
    }
}

背包容量的每一个值,都是经过1 和 5的计算,包含了 {1,5} 和 {5,1}两种情况。此时dp[j]里算出来的就是排列数!

5.举例推导dp数组

输入:amount = 5,coins = [1,2,5],dp状态图如下:

dp[amout]为最终结果

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount + 1, 0);
        dp[0] = 1;
        for (int i = 0; i < coins.size(); i++) { // 遍历物品
            for (int j = coins[i]; j <= amount; j++) { // 遍历背包
                dp[j] += dp[j - coins[i]];
            }
        }
        return dp[amount];
    }
};
  • 时间复杂度:O(mn),其中 m 是 amount,n是coins的长度
  • 空间复杂度:O(m)

【总结】

本题的递推公式,在 494.目标和 中已经做了详细讲解,而本题的难点主要在于遍历顺序!

  • 在求装满背包有几种方案的时候,确定遍历顺序是非常关键的;
  • 如果求组合数,那就是外层for循环遍历物品,内层for循环遍历背包
  • 如果求排列数,那就是外层for循环遍历背包,内层for循环遍历物品

来自代码随想录的课堂截图:

参考文章和视频:

代码随想录 (programmercarl.com)

动态规划之完全背包,装满背包有多少种方法?组合与排列有讲究!| LeetCode:518.零钱兑换II_哔哩哔哩_bilibili

猜你喜欢

转载自blog.csdn.net/weixin_41987016/article/details/133381599
今日推荐