01背包问题
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int w[N],m[N],dp[N][N];
int main()
{
int n,v;
cin>>n>>v;
for(int i=1;i<=n;i++)
cin>>m[i]>>w[i];
for(int i=1;i<=n;i++)
for(int j=v;j>=0;j--)
{
dp[i][j]=dp[i-1][j];
if(m[i]<=j) dp[i][j]=max(dp[i][j],dp[i-1][j-m[i]]+w[i]);
}
cout<<dp[n][v]<<endl;
}
当物品有四种,背包总重量为8时,对于如下的物品
可以通过表将各种情况下的最大值列出来
背包问题的核心代码就是
dp[i][j]=dp[i-1][j];
dp[i][j]=max(dp[i][j],dp[i-1][j-m[i]]+w[i]);
通过遍历可以选取的数量和背包的剩余重量再搭配上我们的核心代码就可以得到上图中的数据。
至于为什么核心代码是那样的,我们通过上图中的数据来理解。
当我们i=3,j=6时,最大值要在dp[i][j]与dp[i-1][j-m[i]]+w[i]中选较大的值,此时dp[i][j]表示的是dp[2][5]的值,也就是7。dp[i-1][j-m[i]]+w[i]的值为dp[2][2]+5,结果为8。相比之下8更大,所以这个空填8。
dp[i-1][j-m[i]]+w[i]的作用是,找到可以选取数量比当前可以选取数量少一个,并且选取剩余体积刚好能装下当前序号的物品的情况,加上当前序号物品的价值。所以每次的情况对应的值都是当前情况的最优值。
优化
.01背包问题的时间复杂度很难优化,但可以对其空间进行优化。可以将dp的二维数组用一维数组代替,用剩余的重量来表示状况,当每个物品的被选取情况发生变化时,数组内存的会是较大的值,不断地更新成最大的值。
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int w[N],m[N],dp[N];
int main()
{
int n,v;
cin>>n>>v;
for(int i=1;i<=n;i++) cin>>m[i]>>w[i];
for(int i=1;i<=n;i++)
for(int j=v;j>=m[i];j--)
{
if(m[i]<=j) dp[j]=max(dp[j],dp[j-m[i]]+w[i]);
}
cout<<dp[v]<<endl;
}
完全背包问题
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
10
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int dp[N],n,v,m[N],w[N];
int main()
{
cin>>n>>v;
for(int i=1;i<=n;i++) cin>>m[i]>>w[i];
for(int i=1;i<=n;i++)
for(int j=m[i];j<=v;j++)
dp[j]=max(dp[j],dp[j-m[i]]+w[i]);
cout<<dp[v]<<endl;
}
和01背包的代码很相似,只要将剩余体积从大到小循环改成从小到大循环就行。但其中的思路与01背包的思路还是有差别的。
从小到大是因为,之后的选择是基于当前选择而更新的。先将剩余体积从m[i]到v枚举一遍,当前体积刚好能装一个序号为i的物品时,则将其填入数组,然后剩余体积刚好又能装一个时,再将其填入数组。这样就达到了选多个物品的要求。在这基础上分别将不同序号的物品遍历比较,取剩余体积一定时,价值最大的情况填入数组。最后剩余体积为v的情况就为最大值。
多重背包问题
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<vi,wi,si≤100
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10
#include<iostream>
#include<algorithm>
using namespace std;
const int N=110;
int dp[N],n,v,m[N],w[N],k[N];
int main()
{
cin>>n>>v;
for(int i=0;i<n;i++) cin>>m[i]>>w[i]>>k[i];
for(int i=0;i<n;i++)
for(int j=v;j>=0;j--)
for(int c=1;c<=k[i]&&c*m[i]<=j;c++)
dp[j]=max(dp[j],dp[j-c*m[i]]+c*w[i]);
cout<<dp[v]<<endl;
}
当数据量小的时候,可以将不同数量也作为一次循环来当做01背包问题做。
多加的循环是不同的物品取不同数量的价值情况。
当数据量大时,就要使用二进制优化了。
例如一个物品数量为7,可以用1 ,2 ,4三个数来表示1-7之间的取值情况。
用vector容器可以减少内存的使用
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N=2010;
int n,m;
int f[N];
struct Good
{
int v,w;
};
int main()
{
vector<Good> goods;
cin>>n>>m;
for(int i=0;i<n;i++)
{
int v,w,s;
cin>>v>>w>>s;
for(int k=1;k<=s;k*=2)
{
s-=k;
goods.push_back({v*k,w*k});
}
if(s>0) goods.push_back({v*s,w*s});
}
for(auto good: goods)
for(int j=m;j>=good.v;j--)
f[j]=max(f[j],f[j-good.v]+good.w);
cout<<f[m]<<endl;
}
如果不用容器的话,可以替换成普通数组一样可以达成效果
#include<iostream>
#include<vector>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=2010;
int n,m;
int f[N],a[N],b[N];
int x=0,y=0;
int main()
{cin>>n>>m;
for(int i=0;i<n;i++)
{ int v,w,s;
cin>>v>>w>>s;
for(int k=1;k<=s;k*=2)
{s-=k;
a[x]=k*v;
b[x]=k*w;
x++;}
if(s>0)
{a[x]=s*v;
b[x]=s*w;
x++;}}
for(int q=0;q<=x;q++)
for(int j=m;j>=a[q];j--)
f[j]=max(f[j],f[j-a[q]]+b[q]);
cout<<f[m]<<endl;
}
将数量用二进制优化法来表示,这样将多个数量的一个物品变成了一个数量的多个体积,价值一样的物品,在使用01背包的方法,就能找到最大值。