记录背包问题的学习过程,第一次发博客,以后有时间研究了再整理。
一、01背包问题
题目大意:给出一个容量为m的背包以及n个物品,每一个物品有所占的体积和其对应的价值,要求出如何选择物品才能使得背包能存放的价值最大。
为什么叫01背包呢?因为在我们进行物品的选择的时候,对于每一个物品都有拿与不拿两种选择,其对应了二进制中的0和1两种状态。首先对于背包容量较小的背包,我们可以采用回溯法解决。但是当物品数量较多,背包容量较大的情况,就显然要超时,因此我们需要采用一种更加优秀的策略:动态规划。
这个问题具有最有子结构,因为最后的方案可以拆分成一件一件去考虑(显然)。当我们考虑的只是最后的最大价值的时候,问题便不再注重过程本身,前面做的选择也不会对后面的有影响就满足了无后效性。
定义状态dp(i,j)为当考虑到前i件物品且背包总容量为j时,所能取得的最大价值。那么这个时候,对于第i件物品本身,我们面临两种情况:1.当前背包容量不足以放入该物品,显然dp(i,j)=dp(i-1,j)。2.当前背包容量足以放入该物品,我们又面临两个选择:将第i件物品装入背包,那么dp(i,j)=dp(i-1,j-v[i])+w[i](因为要放入第i件物品背包必须留下v[i]的空间,我们我们要取dp(i-1,j-v[i])的值加上第i件物品本身的价值w[i])。如果不将这个物品放入背包,那么答案显然和无法放入背包的情况一致。
状态转移方程:dp(i,j)=max(dp(i-1,j),dp(i-1,j-v[i])+w[i]
记录代码:
// Template 01 backpack
#include <bits/stdc++.h>
using namespace std;
const int mx=205;
int n,m,w[mx],v[mx],dp[205][500];
//n is object number m is backpack volume
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
if(j<w[i])
dp[i][j]=dp[i-1][j];
else
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
}
cout<<dp[n][m]<<endl;
return 0;
}
代码可以进行空间优化:
观察状态转移方程发现考虑第i件物品时,该物品的状态只和上一件物品的状态有关(动态规划具有无后效性,最优子结构,前面的答案已经是最优解)。
朴素想法可以使用滚动数组把数组压至两行。但是这里可以直接让一维数组继承上一次的答案,然后从后往前遍历到第i个刷新当前的值。
忘记可以参见这篇博客:
01背包空间优化,完全背包时间优化 的图解_AkagiSenpai的博客-CSDN博客
那么我们只存一行就好了啊,而且问题来自于上一行 j 以及前面的区域,我们必须让 j 以递减的形式更新,以保证能够取到上一行的前面的值(因为 j 递减更新的话前面是旧值,我们恰恰需要上一行的旧值。这句话特别精辟。
//Let's optimize it
#include <bits/stdc++.h>
using namespace std;
const int mx=205;
int n,m,w[mx],v[mx],dp[5005];
//n is object number m is backpack volume
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i];
for(int i=1;i<=n;i++)
for(int j=m;j>0;j--)
if(j>=w[i])
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
cout<<dp[m]<<endl;
}
二、完全背包问题
题目大意与01背包基本一致,不过与01背包不同的是,完全背包的物品不只有一件,从01背包的每件变成了每种,每种物品可以无限取用。
考虑01背包做法:当我们考虑的第i件物品空间为j时,我们先放入一件,如果背包中仍有足够空间,那么我们再放入一件,知道无法再放入为止。
状态转移方程:dp(i,j)=max(dp(i-1,j),dp(i,j-v[i])+w[i])
记录代码:
// Template Completely backpack
#include <bits/stdc++.h>
using namespace std;
const int mx=50;
int v[mx],w[mx],dp[mx][mx],n,m;
//n is object number m is backpack volume
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>v[i]>>w[i];
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
if(j<v[i])
dp[i][j]=dp[i-1][j];
else
dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+w[i]);
}
}
cout<<dp[n][m]<<endl;
}
类似01背包一样的优化:
//Let's optimize it
#include <bits/stdc++.h>
using namespace std;
const int mx=50;
int v[mx],w[mx],dp[mx],n,m;
//n is object number m is backpack volume
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>v[i]>>w[i];
for(int i=1;i<=n;i++){
for(int j=v[i];j<=m;j++){
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
}
}
cout<<dp[m]<<endl;
}
这里要注意的是j是从0开始的,01背包时我们想使用的是旧值,而在完全背包中我们则想使用新值,所以从0开始的目的在于刷新dp[j]前面的值以便于我们能够取用。
完全背包问题(详细解答)_曼切斯特的流氓的博客-CSDN博客_完全背包问题
方程推导什么的见这位大佬博客。
三、多重背包问题
题目大意与完全背包基本一致,但是把多重背包中的每种物品可以无限取用变成了每种物品可以取用有限次,且可以取用的次数有可能不同。
考虑完全背包做法:就是再每一个物品本身没有限制的背后加一个限制,如果当前背包容量足够且物品还有时考虑再取,不满足这两个条件便不再取。
状态转移方程:dp(i,j)=max(dp(i-1,j-k*v[i])+k*w[i]) (0<=k<=p[i])
记录代码(已经一步优化到位了):
#include <bits/stdc++.h>
using namespace std;
int n,m,dp[505];
int main()
{
cin>>n>>m;
for(int i=1,a,b,c;i<=n;i++){
cin>>a>>b>>c;
for(int j=m;j>=a;j--)
for(int k=0;k<=c&&k*a<=j;k++)
dp[j]=max(dp[j],dp[j-k*a]+k*b);
}
cout<<dp[m]<<endl;
}
考虑01背包做法(这个方法也叫二进制优化):这个题还可以对算法进行优化。我们考虑01背包做法,把每一类物品拆成一个一个的物品,可以采用01背包进行处理。但是不难发现这种处理方法反而增加了物品的数量,算起来比完全背包要慢。考虑二进制思想:我们知道,每一个十进制数字都可以由一串二进制数字表示。 我们考虑像二进制一样分割这这些物品,做法如下:将第种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为
,且
是满足
的最大值。如果p[i]=13,则将其分成1,2,4,6。显然1到13当中的任何一个数字可以由这四位数字当中的任意组合得到。这样就大大减少了物品的数量,提升了代码的效率。
记录代码:
#include <bits/stdc++.h>
using namespace std;
int n,m,dp[2005];
int main()
{
cin>>n>>m;
for(int i=1,a,b,c;i<=n;i++){
cin>>a>>b>>c;
int num=min(c,m/a);
for(int k=1;num>0;k<<=1){
if(k>num)
k=num;
num-=k;
for(int j=m;j>=a*k;j--)
dp[j]=max(dp[j],dp[j-a*k]+b*k);
}
}
cout<<dp[m]<<endl;
}
总结
更新ing....