目录
一、前言
感谢你能点开这篇文章,并与我们一起学习!
本篇文章继上篇动态规划(一),由01背包往完全背包深入了解。
二、完全背包
• 什么是完全背包?
完全背包,是一种经典的信息学问题,是研究一个固定容量的背包内能装多大价值的东西的问题。
跟01背包区别的是:完全背包可以重复的选择同一样物品
这时我们需要考虑的是:物品体积?物品价值?背包体积?对于选择几次不做考虑,遍历背包是会对一个物品多次选择;
(1)518. 零钱兑换 II
leetcode传送➡️https://leetcode.cn/problems/coin-change-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
1. dp数组定义
本题与动态规划(一)中的“目标和”题目思路是一样的,但是这里的物品可以选择多次,所以属于完全背包问题。
题目出现总金额,我们不难想到将总金额作为dp背包的体积,又因为这道题要求我们求满足条件的方法个数,所以dp背包的含义为:装满背包的方法个数;
2. 递推公式
装满该背包体积的物品,由减去该物品体积的另一被装满的背包加上该物品体积所得;同时我们时刻注意dp的含义——方法个数,及推导出,递推公式为:
dp[j] += dp[j - coins[i]];
这里可能有人会对后面+1,但是仔细想,一个装满的背包加上一个物品来到另一体积更大的背包,方法数其实还是装满之前体积较小的背包的方法数。
3. 初始化
dp[0]表示体积为0的背包装满的方法数,考虑到递推到 dp[0] 时是直接加上该背包的方法数的,所以我们需要对dp[0]初始化为1
举例:体积为 5 的物品由 dp[0] 加到 dp[5]
dp[5] += dp[5 - 5];
(如果dp[0]初始化为0的话,通过递推后的答案依然是0 )
4. 遍历顺序
由于是完全背包,一个物品可以被使用多次,所以遍历顺序与01背包有很大的不同,需要从前往后遍历背包;
又因为该题选择物品的顺序不做要求,及求组合数,所以要先遍历物品后背包
• 整体代码
int change(int amount, int* coins, int coinsSize)
{
//初始化
int dp[amount + 1];
memset(dp, 0, sizeof(dp));
dp[0] = 1;
for (int i = 0; i < coinsSize; i++) //先物品
{
for (int j = coins[i]; j <= amount; j++) //后背包,从前往后
{
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
(2)322. 零钱兑换
leetcode传送➡️https://leetcode.cn/problems/coin-change/
给你一个整数数组
coins
,表示不同面额的硬币;以及一个整数amount
,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回
-1
。你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11 输出:3 解释:11 = 5 + 5 + 1
1. dp数组定义
同样是零钱兑换,但是Ⅰ与Ⅱ有很大的区别;
由题可知,这里的dp的含义为:装满背包所需的最少物品(钱币)数量;
2. 递推公式
重新选择一个物品来装满这个背包 与 已经被装满的背包比较取较小值作为装满这个背包的最少钱币数量,及递推公式为:
dp[j] = MIN(dp[j - coins[i]] + 1, dp[j]);
注意这里的+1,并不是方法数,而表示多加了一枚硬币
3. 初始化
• 与其它题不同的是,这里需要求最小值,所以我们不能对dp都初始化为0,而需要对其初始化为一个较大值:
这里可以初始化为MAX_INT;
也可以初始化为目标值加一(amount+1),因为钱币值为整数,最小为1,所以要达到目标和的最大钱币数肯定比amount+1小
• dp[0]表示装满背包体积为0所需最小钱币数量,自然初始化为0
4. 遍历顺序
遍历顺序与上一题相同,这类组合加完全背包均可考虑这样的遍历顺序
• 整体代码
#define MIN(x,y) (x) > (y) ? (y) : (x)
int coinChange(int* coins, int coinsSize, int amount)
{
//初始化
int MAX = amount + 1;
int dp[amount + 1];
for (int i = 0; i <= amount; i++)
{
dp[i] = MAX;
}
dp[0] = 0;
for (int i = 0; i < coinsSize; i++)
{
for (int j = coins[i]; j <= amount; j++)
{
if (dp[j - coins[i]] != MAX) //排除最大值+1
{
dp[j] = MIN(dp[j - coins[i]] + 1, dp[j]);
}
}
}
if (dp[amount] == MAX) 这里表示没有能使背包装满的钱币组合
return -1;
return dp[amount];
}
(3)377. 组合总和 Ⅳ
leetcode传送➡️https://leetcode.cn/problems/combination-sum-iv/
给你一个由 不同 整数组成的数组
nums
,和一个目标整数target
。请你从nums
中找出并返回总和为target
的元素组合的个数。题目数据保证答案符合 32 位整数范围。
示例 1:
输入:nums = [1,2,3], target = 4 输出:7 解释: 所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1) 请注意,顺序不同的序列被视作不同的组合。
1. dp数组定义
由题,dp背包大小为target;
这道题的dp含义与打家劫舍Ⅱ的dp含义相似:装满背包的方法数(组合数)
2. 递推公式
本题递推公式与打家劫舍Ⅱ也是相同的,也是求方法数:
dp[i] += dp[i - nums[j]];
3. 初始化
同样的道理,求方法数:dp[0]初始化为1,其余初始化为0
4. 遍历顺序
通读一遍题,其实他叫我们求的是不同的排列数;
所以与之前不同的是,我们需要先遍历背包,后遍历物品
至于为什么呢?先便利背包,得到一个背包,由这个背包遍历物品,每个物品(体积比背包小)都能由另一背包递推到该背包中,得到的便是排列的数量
大家可以举一些例子来区别排列与组合两者遍历顺序的区别,这点是非常重要的。✔️
•整体代码
#define INT_MAX 2147483647
int combinationSum4(int* nums, int numsSize, int target) {
//初始化
int dp[target + 1];
memset(dp, 0, sizeof(dp));
dp[0] = 1;
for (int i = 1; i <= target; i++) //先背包,从前往后
{
for (int j = 0; j < numsSize; j++) //后物品
{
if (nums[j] <= i && (dp[i] + dp[i - nums[j]]) < INT_MAX) //排除
{
dp[i] += dp[i - nums[j]];
}
}
}
return dp[target];
}
※拓展
在学习动态规划基础时,相信大家做过青蛙跳台阶的题目,那道题是青蛙一次可以跳一阶或者两阶,那么拓展到 n阶 该怎么做呢?其实解法与这道题是一模一样的,大家可以自行根据这道题的解法摸索出来。
01背包与完全背包及解题总结💐💐💐
递推公式:总结01背包与完全背包的题型,主要是求最大价值和方法个数,这里我们将这两类的递推公式模板总结出来,具体问题还是要具体分析
①最大价值题:
dp[j] = max(dp[j], dp[j-weight[i]] + value[i])
②方法个数题:
dp[j] += dp[j - nums[i]]
初始化:通过上面这一系列题,我们也发现了初始化的一些规律,具体问题还是要具体分析
①最大价值题:
memset(dp,0,sizeof(dp));
②方法个数题:
memset(dp, 0, sizeof(dp));
dp[0] = 1;
遍历顺序:
① 01背包:从后往前遍历背包; 完全背包:从前往后遍历背包;
② 求组合数:先遍历物品后遍历背包 求排列数:先遍历背包后遍历物品。