01背包问题
基本题型:背包的体积为m,有n个物品它们有不同的价值v和不同的体积w,计算背包最多能装多少价值的物品。
和贪心的区别:贪心求解背包问题用的性价比,性价比优的先装,剩下的物品可以拆分直到装满背包。动态规划01背包问题只有两种状态,1装0不装,物品不可拆分。由于只有装与不装两种状态,无需排序,但如果增加其他限制条件,也可能需要考虑排序问题。
普通方法:
memset(d,0,sizeof(d));
for(i=1; i<=n; i++)
{
for(j=0; j<=m; j++)
{
if(j-w[i]>=0)
d[i][j]=max(d[i-1][j],d[i-1][j-w[i]]+v[i]);
else d[i][j]=d[i-1][j];
}
}
cout<<d[n][m]<<endl;
用d[i][j]描述状态,i表示前i件物品,j表示背包的承重。因为物品只存在放(1)与不放(0)两种状态:
0时d[i][j]=d[i-1][j]
,因为第i个物品不放,相当于i-1的物品放到承重为j的背包里。
1时d[i][j]=d[i-1][j-w[i]]+v[i]
其中j-w[i]相当于为第i个物品预留这个物品的空间。
由i和i-1想到用滚动数组减少空间复杂度,进而想尝试用一维数组减化这个问题。分析发现可以去掉i这一维,只保留后面的j变成一维数组,先上模板:
memset(d,0,sizeof(d));
for(i=1; i<=n; i++)
{
for(j=m; j>=w[i]; j--)
{
d[j]=max(d[j],d[j-w[i]]+v[i]);
}
}
cout<<d[m]<<endl;
为什么内层循环要倒过来写呢?
参考其他博主图片
自己做了一个简单模拟,和上面图片思路相同
我们发现,当内层循环倒着写的时候,我们求的d[j]还保存着前一个状态(i-1)的各个d[j]的值,相当于实现了d[i-1][j-w[i]]
的功能。如果不倒着写,当在求同一个物品i的各种状态j时,由于j是从小到大循环的,所以j取的越大时必定会受到同一个i的小j的影响,举个例子:上面的动态转移方程d[j]=max(d[j],d[j-w[i]]+v[i])
,当i=1时,因为for(j=w[i];j<=m;j++)
,w[1]=2,v[1]=6,首先更新d[2]=d[2-2]+v[1]=0+6=6,接着d[3]=d[3-2]+v[1]=0+6=6,但是到了d[4]=d[4-2]+v[1]=6+6=12,物品i的各个j受到的影响不是来自i-1个物品的,而是来自i本身,不符合01背包的逻辑,但符合完全背包的逻辑。
参考来源
#include<iostream>
#include<string>
#include<algorithm>
using namespace std;
int a[1005],b[1005],d[1005];
int main()
{
int t,i,n,v,j;
cin>>t;
while(t--)
{
cin>>n>>v;
for(i=1; i<=n; i++)
cin>>a[i];
for(i=1; i<=n; i++)
cin>>b[i];
memset(d,0,sizeof(d));
for(i=1; i<=n; i++)
{
for(j=v; j>=b[i]; j--)
{
d[j]=max(d[j],d[j-b[i]]+a[i]);
}
}
cout<<d[v]<<endl;
}
return 0;
}
还不明白可参考这个博主的博客,总结的比较详细:
01背包问题相关优化大全(二维到一维)
需要用到排序+01背包的问题:
E
题意:有n件物品可买,你有m钱,告诉你这n件物品每件的价格pi,价值vi,以及你至少有多少钱才能买这件物品qi,求你最多能得到多少价值
•为什么排序:举个例子
3 10
5 10 5
3 5 6
2 7 3
01背包问题的内循环是倒着推的,如果不排序
本质上相当于倒着买,先买价值为3的再买价值为6的,一共只能买9价值的物品,但我们发现如果先买价值为5的再买价值为6的也可以,一共能买价值为11的物品,显然价值更大。
•怎样排序:
为了避免这种情况,我们应该先排序,把花费大的及可能买的尽量排在后面,因为这样能保证花费小的已经购买了且花费大的也能购买,但是直接这样排是不对滴,分析一下,发现如果知道a物品(pa,qa,va)和b物品(pb,qb,vb),如果你想先买a物品再买b物品,你手里至少要有pa+qb元,如果你想先买b物品再买a物品,你手里至少要有pb+qa元,比较pa+qb与pb+qa取小值,如果pa+qb<pb+qa,即qb-pb<qa-pa,先买a,
按照以上排序,先买q-p大的更合适。
#include<iostream>
#include<algorithm>
#include<minmax.h>
using namespace std;
struct goods
{
int p,q,v;
bool operator<(const goods &b)const
{
return q-p<b.q-b.p;
}
} a[505];
int d[5005],n,m,i,j;
int main()
{
while(cin>>n>>m)
{ memset(d,0,sizeof(d));
for(i=1; i<=n; i++)
cin>>a[i].p>>a[i].q>>a[i].v;
sort(a+1,a+1+n);
for(i=1; i<=n; i++)
{ for(j=m; j>=a[i].q; j--)
d[j]=max(d[j],d[j-a[i].p]+a[i].v);
}
cout<<d[m]<<endl;
}
return 0;
}
B
题意:食堂中有n种菜出售,每种菜可购买一次。已知每种菜的价格以及卡上的余额,问最少可使卡上的余额为多少。注意:卡上的剩余金额大于或等于5元是才可以购买成功(即使购买后卡上余额为负),否则无法购买(即使金额足够)
因为题目中给的限制条件是卡上剩余金额大于等于5元才能购买成功,即使买后为负。为了让剩余金额最小,我们可以先买一些菜,最后剩下5元用来买最贵的菜(一定能买)。
所以先对金额排序,不考虑最贵的菜,只考虑卡上有m-5的钱,这就成了基本的01背包问题,最后再减去最贵的菜就可。
#include<iostream>
#include<algorithm>
#include<string>
using namespace std;
int v[1005],m,d[1005];
int main()
{
int n,i,j;
while(cin>>n&&n!=0)
{
for(i=1; i<=n; i++)
cin>>v[i];
cin>>m;
sort(v+1,v+1+n);
memset(d,0,sizeof(d));
for(i=1; i<n; i++)
{
for(j=m-5; j>=v[i]; j--)//m-5保证可以购买成功
{ d[j]=max(d[j],d[j-v[i]]+v[i]);
}
}
if(m<5)
cout<<m<<endl;
else
cout<<m-d[m-5]-v[n]<<endl;
}
return 0;
}