01背包问题
问题重述:
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
解题思路:
经典的动态规划,我们需要找到一个合理的当前状态以及相应的状态转移方程。已知信息是物品数量N,背包总体积V,以及各个物品的价值。
找合适的状态
我们来尝试拆分出子问题,物品价值是物品的属性拆分它没有价值,那我们就来拆分N与V看看能不能分解出子问题,实际上是可以的,总问题是在物品数为N体积不超过V的情况下最大价值是多少。那我们就可以设想能否求出物品数为N-1体积不超过V-1的情况下最大价值是多少,这样我们就找到了我们的当前状态的表示方法,即用 f[ i ][ j ] 来表示前i个物品中在体积不超过j的情况下最大的价值是多少。
找状态转移方程
找到了当前状态的表示,我们便很容易理清边界情况,即当 i 或 j 为 0 时 f[ i ][ j ] 的值便为 0 ;接下来我们思考状态转移方程 f[ i ][ j +1] 能否通过 f[ i ][ j ] 得到呢?容量加一我们并不清楚之前的物品选择状态是什么,所以我们无法由 f[ i ][ j ] 得到 f[ i ][ j +1]。我们转换思路考虑 f[ i +1][ j ] 能否通过 f[ i ][ j ] 得到,我们发现是可以的,因为f[ i +1][ j ] 是比 f[ i ][ j ] 多一个可选物品的的状态。那么这个物品就有两种情况选与不选,如果我们不选这个物品那么f[ i +1][ j ] 的值就是 f[ i ][ j ] ,如果选了那么它的值就是选了这个物品的情况即f[ i ][ j -v[i]]+wi 那它是否可知呢,答案是肯定的,因为我们外层循环我们的i,内层就得循环我们的 j 因此当我们求到 f[ i ][ j ] 时 f[ i ][ j -x] (x<j) 早已求出。因此我们就能得到我们的状态转移方程即 f[ i ][ j ] = max( f[ i -1][ j ] , f[ i -1][ j -v[i]]+wi ) 。
c++代码:
#include<iostream>
#include<vector>
using namespace std;
int volume[1003];
int value[1003]; //根据数据范围定义体积,价值表示第i个物品的体积,价值
int ans[1003][1003];//根据数据范围定义当前状态ans[i][j]表示前i个物品在体积不超过j的前提下的最大价值
int main()
{
int i,j,N,V;
cin>>N>>V; //定义物品数量和背包体积
for(i=1;i<=N;i++)
{
cin>>volume[i]>>value[i]; //输入各个物品的体积和价值
}
for(i=1;i<N+1;i++) //从第一个物品开始递推找前i个物品的各个状态值
{
for(j=0;j<=V;j++) //求体积从0到V各个状态的值
{
ans[i][j]=ans[i-1][j];//假设第i个物品不选ans[i][j]就等于ans[i-1][j]
if(j>=volume[i]) //当体积能装下这个物品时那么这个物品就有可能被选择
ans[i][j]=max(ans[i-1][j],ans[i-1][j-volume[i]]+value[i]);
//此时这个物品选与不选取决于两种状态谁的值更大
}
}
cout<<ans[N][V]<<endl;//输出我们的答案即前N个物品体积不超过V的情况下的最大价值
return 0;
}
改进我们的代码
观察我们的代码是否可以变得更简洁些呢?我们发现当我们在求 f[ i ][ j ] 时,我们只用到了 f[ i -1][ j ] 和 f[ i-1 ][ j -v[i]] 两个值而我们最终只要能求出 f[N][V] 即可 。 所以我们可以不用二维数组,我们可以用一个一维数组 f[j] 表示前 i 个物品情况下的状态就行了,随着我们i的增加不断更新我们的数组就行了。因为我们要更新这个数组即要不断覆盖原来的值,此时体积就不可以从 0 到 V 开始了因为因为在求 f[j] 可能会用到 f[ j-v[i] ] 所以不能从小到大覆盖 f[j] 因为小的被覆盖了大的就求不出了,所以我们应该从大到小覆盖。
思路如下:
最终代码:
#include<iostream>
#include<vector>
using namespace std;
int N,V;
int ans[1003];//根据数据范围定义当前状态ans[j]表示前i个物品在体积不超过j的前提下的最大价值
int main()
{
cin>>N>>V;
for(int i=0;i<N;i++)
{
int volume,value;
cin>>volume>>value;//输入第i个物品的信息
for(int j=V;j>=volume;j--)//体积从大到小遍历到当前体积
{
ans[j]=max(ans[j],ans[j-volume]+value);//转移方程
}
}
cout<<ans[V]<<endl;//最后的ans[N]即答案
return 0;
}
二维背包问题
问题重述:
有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数,N,V, M,用空格隔开,分别表示物品件数、背包容积和背包可承受的最大重量。
接下来有 N 行,每行三个整数 vi,mi,wi,用空格隔开,分别表示第 i 件物品的体积、重量和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N≤1000
0<V,M≤100
0<vi,mi≤100
0<wi≤1000
思路分析:
在了经典的01背包问题后,那么这个二维背包也就不难了,无非就是在加上一层循环,可以相当于我们对每一种体积都遍历一遍体积或者可以对每一种体积都遍历一遍重量。只要我们最外遍历的是物品里面的循环先后无所谓,因为他们都是物品的属性而已,对于计算机来说它并不知道体积和重量的区别是啥。所以我们的套路就是先遍历物品后依次遍历物品属性,在没有优化我们的代码前我们则需要一个三维数组来存储答案了。现在我们可以用优化的方法,用一个二维数组滚动存储答案。
C++代码:
#include<iostream>
#include<vector>
using namespace std;
int main()
{
int N,V,M;
vector< vector<int> >ans;
cin>>N>>V>>M;//输入规模物品数量、背包的体积和背包的承重
ans.resize(V+10,vector<int>(M+10,0));//用容器开辟合适空间的二维数组滚动储存答案也可以直接定义合适大小的二维数组一般多开几个空间
for(int i=0;i<N;i++)//开始遍历物品
{
int Vi,Mi,Wi;
cin>>Vi>>Mi>>Wi;//现场输入物品属性体积、重量和价值
for(int j=V;j>=Vi;j--)//从大到小遍历体积
{//虽然这里的两种循环可以任意换但是二维数组的格式必须和它们的先后一致
for(int k=M;k>=Mi;k--)//对每一种体积从大到小遍历重量
{
ans[j][k]=max(ans[j][k],ans[j-Vi][k-Mi]+Wi);//二维状态转移方程找到当前体积当前重量下的最大价值
}
}
}
cout<<ans[V][M]<<endl;//输出答案
return 0;
}