【LeetCode动态规划#09】完全背包问题实战,其二(零钱兑换和完全平方数--求物品放入个数)

发布时间 2023-04-19 22:12:32作者: dayceng

零钱兑换

力扣题目链接(opens new window)

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。

示例 1:

  • 输入:coins = [1, 2, 5], amount = 11
  • 输出:3
  • 解释:11 = 5 + 5 + 1

示例 2:

  • 输入:coins = [2], amount = 3
  • 输出:-1

示例 3:

  • 输入:coins = [1], amount = 0
  • 输出:0

示例 4:

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

示例 5:

  • 输入:coins = [1], amount = 2
  • 输出:2

提示:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 2^31 - 1
  • 0 <= amount <= 10^4

思路

硬币可以无限次使用,典型的完全背包问题(正序遍历)

注意这次是要求放入硬币的个数,最少个数

不多说直接看

五步走

1、确定dp数组的含义

dp[j]:能够凑出总金额j的最少硬币数

2、确定递推公式

如何推导出dp[j]? 这里还是可以分为放入硬币不放入硬币两种情况(详见

(1)不放入

如果不放入硬币,那么就还是取上一层遍历递推的结果,即dp[j] (体现用一维数组解决背包问题时,数组的“滚动”,可以联系 零钱兑换II 的解释)

即dp[j] = dp[j];

(2)放入硬币

假设我们现在遍历到j - coins[i]的容量,此时能凑出总金额j - coins[i]的最少硬币数是dp[j - coins[i]]

我们要求的是总金额为j(即容量为j)的最少硬币数,显然只需j - coins[i] + coins[i]即可,也就是再放一个硬币进背包。

那么相当于此时遍历到了背包容量j,又因为硬币放入背包时要占空间的,所以背包容量还是可以表示为j - coins[i]

此时,能凑出总金额j的最少硬币数是dp[j - coins[i]] + 1,加的1是指放入的这个硬币

即dp[j] = dp[j - coins[i]] + 1;

由于我们求的是最少硬币数,因此需要取这两者中更小的那个,那么递推公式就是

dp[j] = min(dp[j], dp[j - coins[i]] + 1);

3、初始化dp数组

这里不要惯性思维直接把dp[0]初始化为1,先看看题目

题目给的示例中明确了,dp[0] = 0

那其他位置呢?因为我们要找的的是最少硬币数,也就是递推过程中的最小值

为了不影响最小值的更新,应该将其余部分的值初始化为最大整数INT_MAX

4、确定遍历顺序

本题求钱币最小个数,那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数

所以本题并不强调集合是组合还是排列。

代码

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        //定义dp数组并初始化
        //因为要找的是更小的递推值,所以初始值应该设为最大值
        vector<int> dp(amount + 1, INT_MAX);
        //由题目给的示例可知,dp[0]时应该是0
        dp[0] = 0;

        //遍历dp数组,顺序无所谓,因为要找的是使用硬币的最少个数
        for(int i = 0; i < coins.size(); ++i){
            for(int j = coins[i]; j <= amount; ++j){
                if (dp[j - coins[i]] != INT_MAX) { // 如果dp[j - coins[i]]是初始值则跳过
                    dp[j] = min(dp[j], dp[j - coins[i]] + 1);//注意这里找的是硬币个数
                }
            }
        }
        //如果递推公式推导最后发现最后的值还是初始值,说明没有找到组合,返回-1
        if (dp[amount] == INT_MAX) return -1;
        return dp[amount];
    }
};

完全平方数

力扣题目链接(opens new window)

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

示例 1:

  • 输入:n = 12
  • 输出:3
  • 解释:12 = 4 + 4 + 4

示例 2:

  • 输入:n = 13
  • 输出:2
  • 解释:13 = 4 + 9

提示:

  • 1 <= n <= 10^4

思路

"若干个完全平方数"---物品

"给定正整数 n"---背包容量

又由示例1可知,物品可以重复使用,因此这是一个完全背包

五步走

1、确定dp数组含义

dp[j]:给定整数j时,能放入使和为j的完全平方数的最少数量是dp[j]

2、确定递推公式

如何推导出dp[j]?

当然还是从两个方向:加上当前的完全平方数或者不加

所以dp[j] = min(dp[j], dp[j - i * i] + 1);这里求的是数量,所以仍然是加1

注意,所谓的"完全平方数"就是整数的平方,每个数的平方都是"完全平方数"

本题是要用"完全平方数"来装背包,所以好获取"完全平方数",只需在背包容量上限内不断循环一个自增的值,然后取平方即可(i*i)

3、初始化dp数组

联系dp定义,dp[0]:给定整数0时,能放入使和为j的完全平方数的最少数量是dp[0]

感觉dp[0]时应该是没有能放进去的(这里理由还没想好,以后可能有补充),并且题目是从1开始给的整数,就没有0这种情况

那么暂定dp[0] = 0(不行再改1),然后其余部分分的初始化还是INT_MAX(原因见上一题)

4、确定遍历顺序

完全背包,正序遍历

然后因为不是求排列或组合,遍历物品和容量时的顺序可以调换

代码

class Solution {
public:
    int numSquares(int n) {
        //定义dp数组
        vector<int> dp(n + 1, INT_MAX);

        //初始化
        dp[0] = 0;

        //遍历dp数组
        for(int i = 1; i*i <= n; ++i){//遍历物品
            for(int j = i*i; j <= n; ++j){//遍历背包容量,
                dp[j] = min(dp[j], dp[j - (i * i)] + 1);
            }
        }
        return dp[n];
    }
};

在代码实现中要注意两层for循环的处理细节

这里我们用来放入的“物品”是"完全平方数",也就是i*i

不像之前的题,我们从一个数组之类的地方把物品取出来放入就行

这里的物品需要使用循环值来“现用现算”

对于i*i <= n,其实就相当于之前的j = coins[i]; j <= amount

第二层for循环也是同理,将i*i这个整体看成物品即可