目录
一.什么是完全背包
完全背包问题一般是指:有N件物品和一个能背重量为W的背包,第i件物品的重量为weight[i],价值为value[i]。每件物品有无限个(也就是可以放入背包多次),求怎样可以使背包物品价值总量最大。
完全背包与01背包问题的区别在于01背包物品只有一个,完全背包有无数个。
完全背包问题与01背包问题大致相同,唯一不同的地方体现在遍历顺序方面。
这里回回顾一下01背包问题的遍历顺序:
//物品
for(int i=1;i<=n;i++){
//背包容量。倒序遍历
for(int j=V;j>0;j--){
//放得下
if(j>=vw[i-1][0]){
dp[j]=max(dp[j],dp[j-vw[i-1][0]]+vw[i-1][1]);
}
//放不下就是原来的值
}
}
注意这里是倒序遍历的,如果不倒序遍历回导致一件物品重复选取。
为什么是重复选取?
后面的最大价值由前面的值得到,前面得值选取了第i件物品,后面容量仍然可以选取第i件物品,导致第i件物品重复选取。
然而完全背包问题正好一个物品有n件,正好需要重复选取,所以遍历顺序为正序遍历,如下:
//物品
for(int i=1;i<=n;i++){
//背包容量。正序遍历
for(int j=0;j<=w;j--){
//放得下
if(j>=vw[i-1][0]){
dp[j]=max(dp[j],dp[j-vw[i-1][0]]+vw[i-1][1]);
}
//放不下就是原来的值
}
}
二.完全背包问题的里外层循环可以交换吗
看上面的到吗可以看到,01背包问题代码有两层循环,第一层循环为第i个物品,第二层循环是背包的容量。这里有一个问题,这里的遍历顺序可以交换吗?
首先知道交换里外层遍历顺序意味着什么?意味着,是按物品更新价值,从数组上表现的是,列不变,这一列的每一行价值更新。
价值更新是由上一行的价值和上一行前面的价值加上现在的价值决定。能不能交换取决于上一行前面的值是否更新。也就是是否可以从里层循环是否可以从前往后遍历。
01背包对于用二维数组保存结果是可以交换遍历顺序的(前提里层遍历顺序从前往后遍历),用优化的滚动一维数组不可以交换遍历顺序,因为滚动的一维数组里层遍历顺序是从后往前遍历的,价值更新由前面的价值决定,并未更新。
对于完全背包问题,可以交换。可以从前往后遍历。
三.题
3.1 求组合数
力扣: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
由题意可知,同一硬币可以无限取,首先我们可以发现这是一个组合问题,可以用回溯算法。但是再力扣给的测试用例中会超过时间限制。
由于同一硬币可以无限次取,并且有一目标数,可以使用完全背包开解。
目标数为背包容量,硬币数值代表物品容量,但是这里求的是方法数,并不是求的价值。
状态定义:到达目标值j的方法数,dp[ i ]。
转移方程:这里求的是方法数,并不是求最大价值。
方法数等于:不取这件物品时到达容量j时的方法数(原先的方法数),加上取这件物品到达容量j时的方法数。取这件物品要腾出空间。
dp[ j ]=dp[ j ]+dp[ j-coins[ i ]]。
初始化:容量为0时不取物品,方法数为1。
返回值:容量为amount时的方法数dp[amount ]。
class Solution {
public:
int change(int amount, vector<int>& coins) {
int len=coins.size();
vector<int> dp(amount+1,0);
//初始化
dp[0]=1;
for(int i=1;i<=len;i++){
//完全背包,正序遍历
for(int j=1;j<=amount;j++){
//求方法数
if(j>=coins[i-1]){
dp[j]+=dp[j-coins[i-1]];
}
}
}
return dp[amount];
}
};
3.2 求排列和
力扣:377. 组合总和 Ⅳ
给你一个由 不同 整数组成的数组 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,2 ]与[ 2, 1 ]是同一组合
排列注重顺序。如[ 1,2 ]与[ 2, 1 ] 不同排列。
这题与上面这题组成鲜明对比:上面一题是组合问题。
这里有一个结论,本文中提到,完全背包问题的里外层遍历顺序是可以交换的。
注意是有方法数,求价值为总和,总和不变的。
当求方法数时:
当外层循环为物品,里层循环为背包容量时,求的是组合数的方法数。
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
dp[j] += dp[j - coins[i]];
}
}
因为当容量够放下第i个物品时,减去的容量coin[ i ]为同一值,不会有其它组合的方法数。
当外层循环为背包容量,里层循环为物品时,求的是排列数的方法数。
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]];
}
}
因为当容量可以放下0~j (j<=i) 个物品时,求的方法数,将所有减去coins[ 0 ]~coins[ j ]的值都求进来了。是所有的排列数。
四个角度我就不分析了,和上面相同,只是遍历顺序变了
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
int len=nums.size();
vector<int> dp(target+1,0);
dp[0]=1;
//排列问题的完全背包问题
//这里要改变遍历顺序,外层遍历背包容量,方法上才将排列的所有方法加上
for(int i=1;i<=target;i++){
for(int j=1;j<=len;j++){
//防止dp[i]越界int
if(i>=nums[j-1]&&dp[i] < INT_MAX - dp[i - nums[j-1]]){
dp[i]+=dp[i-nums[j-1]];
}
}
}
return dp[target];
}
};
dp[i] < INT_MAX - dp[i - nums[j-1]]为了防止dp[i]越界int。
3.3 求最小值
力扣 322. 零钱兑换
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
求硬币个数可以无限取 转化为完全背包问题为,背包容量amount,物品容量coins[ i ],价值为1。
状态定义:达到容量i时,硬币数最少为dp[ i ]。
转移方程:这里求的是硬币个数,不管排列还是组合,个数都一样,所以里外层循环求得都一样。
求得硬币数有两种情况
不取硬币:硬币个数为之前达到容量i时的硬币个数,dp[i]。
取硬币:硬币个数为先腾出要放的硬币容量,再加1。即 dp[ i-coins[i] ]+1
最少硬币数为:min(dp[i],dp[ i-coins[i] ]+1)。
初始化:容量为0时,不放硬币数 dp[ 0 ]=0。也可以理解,后面的方法数都时在这个基础上加1。
容量不为0时,要取一个最大值,因为求的是硬币数的最小值,不初始化为最大值,可能得不到硬币数最小数,而是初始化的值。
//为什么初始化为INT_MAX,求的是最小值,
vector<int> dp(amount+1,INT_MAX);
//为什么初始化为0,每个值都是从容量0开始往上加的
dp[0]=0;
返回值:值为amount是硬币最少数 dp[ amount ]。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int len=coins.size();
//初始化出0外,其它都初始化为INT_MAX,
//为什么初始化为INT_MAX,求的是最小值,
vector<int> dp(amount+1,INT_MAX);
//为什么初始化为0,每个值都是从容量0开始往上加的
dp[0]=0;
for(int i=1;i<=len;i++){
for(int j=1;j<=amount;j++){
//防止越界
if(j>=coins[i-1]&&dp[j-coins[i-1]]<INT_MAX){
dp[j]=min(dp[j],dp[j-coins[i-1]]+1);
//flag=1;
}
}
}
return dp[amount]==INT_MAX?-1:dp[amount];
}
};
dp[j-coins[i-1]]<INT_MAX)为了防止溢出int。
力扣:279. 完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
这个题和上面题做法一样,只是要求一个完全平方数的数组。直接上代码
class Solution {
public:
int numSquares(int n) {
vector<int> nums;
int len=sqrt(n);
//求完全平方数
for(int i=1;i<=len;i++){
nums.push_back(i*i);
}
vector<int> dp(n+1,INT_MAX);
dp[0]=0;
for(int i=1;i<=len;i++){
for(int j=nums[i-1];j<=n;j++){
if(dp[j-nums[i-1]]<INT_MAX){
dp[j]=min(dp[j],dp[j-nums[i-1]]+1);
}
}
}
return dp[n];
}
};