今天是动态规划学习的第五天,主要学习了多重背包问题的原理以及应用,包括多重背包应用中的排列和组合问题的区分。
完全背包
题目链接:52. 携带研究材料(第七期模拟笔试) (kamacoder.com)
完全背包的含义是在01背包的基础上去掉物品只能选取一次的限制,即对于每件物品可以选取无限次,进而求解装满背包的最大价值。多重背包与01背包的主要区别在于递归公式上我们先来回顾一下01背包的递归公式:
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
换做是多重背包问题,我们只需要修改背包容量的遍历顺序,正序遍历背包容量即可。具体代码实现如下所示
// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
还有一点值得注意,对于多重背包问题,外层循环和内层循环的顺序是可以颠倒的,因为dp[j]的数值是根据前面的数值得到的,不管外层循环是背包容量,内层循环是物品;还是外层循环是物品,内层循环是背包容量,dp[j]前面的数值都是正确更新的,所以这个遍历顺序是无关紧要的。这一点需要与01背包进行比较和区别。具体代码实现如下所示:
#include<iostream>
#include<vector>
using namespace std;
int main()
{
int n,v;
scanf("%d%d",&n,&v);
int weight[n];
int val[n];
for(int i=0;i<n;i++) scanf("%d %d",&weight[i],&val[i]);
vector<int> dp(v+1,0);
for(int i=0;i<n;i++)
{
for(int j=weight[i];j<=v;j++)
{
dp[j]=max(dp[j],dp[j-weight[i]]+val[i]);
}
}
printf("%d",dp[v]);
}
518. 零钱兑换 II
题目链接:518. 零钱兑换 II - 力扣(LeetCode)
这个题目是多重背包的一种应用,也牵扯出一个问题,排列和组合的区别。
首先题目是给定一个总金额和不同金额的集合,求解有多少种凑成总金额的方式。抽象出来,其实就是多重背包问题的背景下装满背包的方法有多少种,在01背包问题中我们也遇到了这样的问题。迭代公式如下
dp[j]+=dp[j-coins[i]];
所以我们只需要修改背包容量的遍历顺序,改为正序遍历即可。具体代码实现如下所示:
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount+5,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];
}
};
我做这道题目是很快AC了的,但是看了题解后有了一些新的理解。那就是排列和组合的区别。
组合是不强调顺序的,{1,5}和{5,1}是同一种方法;但是排列是强调顺序的,{1,5}和{5,1}是两种方法。所以,这个题目是不能修改外层循环和内层循环的内容的,外层循环只能是遍历物品,内层循环只能是遍历背包容量,这统计的是组合数;如果颠倒顺序,外层循环是遍历容量,内层循环是遍历物品,统计的就是排列数。我自己写了一段打印的代码,结果可以反映出这一现象。
组合数计算过程(外层遍历物品):
1 0 0 0 0 0
1 1 0 0 0 0
1 1 1 0 0 0
1 1 1 1 0 0
1 1 1 1 1 0
1 1 1 1 1 1
1 1 1 1 1 1
1 1 1 1 1 1
1 1 2 1 1 1
1 1 2 2 1 1
1 1 2 2 3 1
1 1 2 2 3 3
1 1 2 2 3 3
1 1 2 2 3 3
1 1 2 2 3 3
1 1 2 2 3 3
1 1 2 2 3 3
1 1 2 2 3 4
-------------------------
排列数计算过程(外层遍历背包容量):
1 0 0 0 0 0
1 0 0 0 0 0
1 0 0 0 0 0
1 1 0 0 0 0
1 1 0 0 0 0
1 1 0 0 0 0
1 1 1 0 0 0
1 1 2 0 0 0
1 1 2 0 0 0
1 1 2 2 0 0
1 1 2 3 0 0
1 1 2 3 0 0
1 1 2 3 3 0
1 1 2 3 5 0
1 1 2 3 5 0
1 1 2 3 5 5
1 1 2 3 5 8
1 1 2 3 5 9
所以在遇到多重背包问题,求解装满背包的方法数时,我们需要区分改变顺序是否是两种不同的方法,以确定不同的遍历顺序。
377. 组合总和 Ⅳ
题目链接:377. 组合总和 Ⅳ - 力扣(LeetCode)
这个题目就是一个典型的排列问题,(1,1,2),(1,2,1),(2,1,1)属于不同的方法。所以,我们应该在外层循环遍历背包容量,内层循环遍历物品。具体代码如下所示:
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<unsigned int> dp(target+10,0);
dp[0]=1;
for(int i=1;i<=target;i++)
{
for(int j=0;j<nums.size();j++)
{
if(i>=nums[j]) dp[i]+=dp[i-nums[j]];
}
}
return dp[target];
}
};
70. 爬楼梯 (进阶)
题目链接:57. 爬楼梯(第八期模拟笔试) (kamacoder.com)
这个题也是一个排列的问题,所以也是先遍历背包容量,在遍历物品件数;在这个题目中就是先遍历最大阶数,内层循环遍历每次能走的步数。具体代码实现如下所示:
#include<iostream>
#include<vector>
using namespace std;
int main()
{
int m,n;
cin>>n>>m;
vector<int> dp(n+10,0);
dp[0]=1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(i>=j) dp[i]+=dp[i-j];
}
}
printf("%d",dp[n]);
}