背包DP
一、普通0/1背包
1、题面:通常题的大意为,给你一个体积为V的背包,并给出n个物品已经每个物品的体积vi和价值wi,求能获得的最大价值。
2、状态转移:
用f[i][j]表示对1到i个物品进行考虑后,体积为j时可获得的最大价值。
我们应遍历每一个物品,并决定是否买它,对于体积为V时,如果不买第i个物品,那么f[i][j]由f[i-1][j]转移过来,如果买第i个物品,则由f[i-1][j-v[i]]+w[i]转移过来。对每个体积都进行遍历,先将f[i][j]由f[i-1][j]转移过来,如果j>=v[i],则将f[i][j]与f[i-1][j-v[i]]+w[i]进行比较,选最大值。
综上:f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
3、代码:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1010;
int n = 0, m = 0;
//f[i][j]表示前i个物品,体积不超过j时的最大价值
//f[i][j] = max(f[i-1][j], f[i-1][j-v[i]]+w[i])
int f[maxn][maxn] = {};
int v[maxn] = {}, w[maxn] = {};
int main()
{
scanf("%d%d", &n, &m);
for(int i=1; i<=n; i++)
{
scanf("%d%d", &v[i], &w[i]);
}
f[0][0] = 0;
for(int i=1; i<=n; i++)
{
for(int j=1; j<=m; j++)
{
f[i][j] = f[i-1][j];
if(j>=v[i]) f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i]);
}
}
printf("%d", f[n][m]);
return 0;
}
4、优化:
由上述讲解和代码可以看出,对一个物品的考虑和且之和上一个物品有关,也就是说,f[i][]和且只和f[i-1][]有关,那么我们可以由此推出一些G妙的东西:我们首先让f[i][j]继承f[i-1][j],然后就去和f[i-1][j-v[i]]+w[i]比较,如果我们省略掉一维,让f[j]表示体积为j时能获得的最大价值,并对每个物品遍历进行考虑,f[j]自然就继承了对上一个物品考虑的最大值,并直接与f[j-v[i]]+w[i]进行比较即可。
但有一个重点就是,f[j]是由f[j-v[i]],也就是比j小的时候转移过来,那么我们要确保f[j-v[i]]是原来的f[i][j-v[i]],所以体积j要从后往前遍历,以确保不会修改j之前的值,否则就会造成同一个物品买了不只一次的情况。
举个栗子:在背包体积为3,考虑一个体积为1,价值为100的物品的时候,如果正序遍历的话,f[1]由f[0]转移过来了,但是f[2]又由f[1]转移过来了,就造成了买多次的情况,直接G;所以我们就得倒序遍历;
由此可得一个新的状态转移方程:f[j]=max(f[j],f[j-v[i]]+w[i]),(v[i]<=j<=V);
5、优化后代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int n,m,V[N],w[N],f[N],ans;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)scanf("%d%d",&V[i],&w[i]);
for(int i=1;i<=n;i++){
for(int j=m;j>=V[i];j--){
f[j]=max(f[j],f[j-V[i]]+w[i]);
}
}
cout<<f[m];
}
[=========================]
二、完全背包
1、题面:设有n种物品,每种物品有一个重量及一个价值。但每种物品的数量是无限的,同时有一个背包,最大载重量为M,今从n种物品中选取若干件(同一种物品可以多次选取),使其重量的和小于等于M,而价值的和为最大。(与0/1背包不同之处就在于每个物品由无限个)
2、状态转移方程:
f[j]=max(f[j],f[j-v[i]]+w[i]);
问题在于——每个物品有无限个,我怎么去模拟?
其实可以转成0/1背包:我们已知最大体积V,那么每个物品最多买V/v[i]件,那么我们可以对每种物品买几件进行遍历,也就是说:f[j]=max(f[j],f[j-kv[i]]+kw[i]),(k∈[0,j/v[i]]);
3、代码:
#include<bits/stdc++.h>
using namespace std;
int n,m;
int v[30],w[30],f[333];
int main()
{
scanf("%d%d",&m,&n);
for(int i=1;i<=n;++i){
scanf("%d%d",&v[i],&w[i]);
}
for(int i=1;i<=n;++i){
for(int j=1;j<=m;j++){
for(int k=0;k<=j/v[i];k++)f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);
}
}
printf("max=%d",f[m]);
}
4、优化:
在0/1背包时,我们将他从二维优化到了一维,并将体积j倒序遍历,是为了防止f[j]对前面造成影响,否则会造成同一个物品买了不止一个的情况,哎嘿~~~~~~~~,不只买了一个,又没有上限,这不纯纯就是完全背包吗,所以我们只需将0/1背包的体积j遍历从倒序变为正序即可;
5、优化后代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,f[30001],ti[30001],v[30001];
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++)scanf("%d%d",&ti[i],&v[i]);
memset(f,0xcf,sizeof(f));
f[0]=0;
for(int i=1;i<=m;i++){
for(int j=ti[i];j<=n;j++){
f[j]=max(f[j],f[j-ti[i]]+v[i]);
}
}
int maxn=0;
for(int i=1;i<=n;i++)maxn=max(maxn,f[i]);
cout<<"max="<<maxn;
}
[==============================]
三、多重背包
1、题面:
2、状态转移方程:
f[j]=max(f[j],f[j-kv[i]]+kw[i]),(k∈[0,min(c[i],j/v[i])]);
3、代码:
#include <bits/stdc++.h>
using namespace std;
const int N=6100;
int n,m,v[N],w[N],s[N],f[N];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d%d%d",&v[i],&w[i],&s[i]);
for(int i=1;i<=n;i++){
for(int j=1;j<=s[i];j++){
for(int k=m;k>=v[i];k--){
f[k]=max(f[k],f[k-v[i]]+w[i]);
}
}
}
printf("%d",f[m]);
return 0;
}
4、是不是发现多重背包和没有优化的完全背包差不多,那可能差不多嘛,交吧,但凡数据范围大一点,那一T一个不吱声;怎么办?在接受大神的洗礼后懂得了用二进制优化——我们知道,一个数有自己相对应的唯一一个二进制表示法,我们要表达k,k∈[0,min(c[i],j/v[i])],打开我们的计算器,好好的观察:拿8举例子,
8(2^0 +2^1 +2^2 +1),7的二进制(1+2+4),6(1+2+3),5(1+2+2),4(1+2+1),3(1+2),2(1+1),1(1),0(0),显而易见,将k拆分成2^0+ 2^1+……之后,这些数可以表示任意一个小于等于k的数;
所以我们可以将一个数拆分为2^0 +2^1 +……个物品,并进行0/1背包;
注意:像8这样的 2^0 +2^1 +2^2 +1,不要忘记最后剩下的数;
5、优化后代码:
#include<bits/stdc++.h>
using namespace std;
const int N=10005;
int v[N],w[N],n,m,f[N],cnt,vi,wi,shu,ans;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
scanf("%d%d%d",&vi,&wi,&shu);
if(shu>m/vi)shu=m/vi;
for(int j=1;j<=shu;j<<=1)v[++cnt]=j*vi,w[cnt]=j*wi,shu-=j;
if(shu>0)v[++cnt]=shu*vi,w[cnt]=shu*wi;
}
for(int i=1;i<=cnt;i++){
for(int j=m;j>=v[i];j--){
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
for(int i=1;i<=m;i++)ans=max(ans,f[i]);
cout<<ans;
}
[==============================]
四、混合背包
1、题面:一个旅行者有一个最多能用V公斤的背包,现在有n件物品,它们的重量分别是W1,W2,...,Wn,它们的价值分别为C1,C2,...,Cn。有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。(什么背包都有)
2、分析:其实与多重背包几乎一模一样,只需将完全背包的上限即V/v[i]找出来就变成了多重背包,并用二进制加速即可;
3、代码:
#include<bits/stdc++.h>
using namespace std;
const int N=10000;
int n,m,v[N],w[N],f[N],num,cnt;
int main(){
cin>>n>>m;
int vi,wi;
for(int i=1;i<=m;i++){
scanf("%d%d%d",&vi,&wi,&num);
if(!num||num>n/vi)num=n/vi;
for(int j=1;j<=num;j<<=1){
v[++cnt]=j*vi;w[cnt]=j*wi;
num-=j;
}
if(num>0)v[++cnt]=num*vi,w[cnt]=num*wi;
}
for(int i=1;i<=cnt;i++){
for(int j=n;j>=v[i];j--)f[j]=max(f[j],f[j-v[i]]+w[i]);
}
int ans=0;
for(int i=1;i<=n;i++)ans=max(ans,f[i]);
cout<<ans;
}
[==============================]
五:分组背包
1、题面:一个旅行者有一个最多能用V公斤的背包,现在有n件物品,它们的重量分别是W1,W2,...,Wn,它们的价值分别为C1,C2,...,Cn。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
2、分析:分组的0/1背包,每组只能买一个,所以我们要对每组进行遍历,在每组中遍历体积,在遍历体积时对每个物品进行考虑,一定要记住:每组最多只能有一个!;
3、代码:
#include<bits/stdc++.h>
using namespace std;
const int N=301;
int m,n,T,t[11][35],v[N],w[N],f[N];
int main(){
cin>>m>>n>>T;
int cx;
for(int i=1;i<=n;i++){
scanf("%d%d%d",&v[i],&w[i],&cx);
t[cx][++t[cx][0]]=i;
}
for(int i=1;i<=T;i++){
for(int j=m;j>0;j--){
for(int k=1;k<=t[i][0];k++){
if(j>=v[t[i][k]]){
f[j]=max(f[j],f[j-v[t[i][k]]]+w[t[i][k]]);
}
}
}
}
int ans=0;
for(int i=1;i<=m;i++)ans=max(ans,f[i]);
cout<<ans;
}
[==============================]
六:变形例题
1、砝码称重(多重)
题目描述
设有1g、2g、3g、5g、10g、20g的砝码各若干枚(其总重<=1000g)。求出用这些砝码能称出的不同重量的个数,但不包括一个砝码也不用的情况。
输入格式
只有一行,共有六个数,一次为1g、2g、3g、5g、10g、20g砝码的个数
输出格式
只有一行,为称出不同重量的个数
样例
样例输入
1 1 0 0 0 0
样例输出
Total=3
以重量为体积,bool f[N],看是否能到达这个体积,f[j]由f[j-v[i]]转移过来;
状态转移方程:f[j]=f[j]||f[j-v[i]];
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=50005;
int mm[7]={0,1,2,3,5,10,20},n,cnt,v[N],ans;
bool f[N];
int main(){
for(int i=1;i<=6;i++){
scanf("%d",&n);
for(int j=1;j<=n;j<<=1){
v[++cnt]=j*mm[i];
n-=j;
}
if(n>0)v[++cnt]=n*mm[i];
}
f[0]=1;
for(int i=1;i<=cnt;i++){
for(int j=1000;j>=v[i];j--)f[j]=f[j]||f[j-v[i]];
}
for(int i=1;i<=1001;i++)ans+=f[i];
cout<<"Total="<<ans;
}
[=========================]
2、NASA的食物计划(0/1,二维)
题目描述
航天飞机的体积有限,当然如果载过重的物品,燃料会浪费很多钱,每件食品都有各自的体积、质量以及所含卡路里,在告诉你体积和质量的最大值的情况下,请输出能达到的食品方案所含卡路里的最大值,当然每个食品只能使用一次.
输入格式
第一行两个数体积最大值(<400)和质量最大值(<400)
第二行 一个数 食品总数N(<50).
第三行-第3+N行
每行三个数 体积(<400) 质量(<400) 所含卡路里(<500)
输出格式
一个数 所能达到的最大卡路里(int范围内)
样例
样例输入
320 350
4
160 40 120
80 110 240
220 70 310
40 400 22
样例输出
550
分析:0/1背包,但是有两个限制条件,所以要用二维的背包,f[i][j]表示体积为i,质量为j时所能获得的最大卡路里。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=501;
int n,m,v1[N],v2[N],w[N],f[N][N],num,cnt;
int main(){
cin>>n>>m;
int N1;
cin>>N1;
for(int i=1;i<=N1;i++){
scanf("%d%d%d",&v1[i],&v2[i],&w[i]);
}
for(int i=1;i<=N1;i++){
for(int j=n;j>=v1[i];j--){
for(int k=m;k>=v2[i];k--)f[j][k]=max(f[j][k],f[j-v1[i]][k-v2[i]]+w[i]);
}
}
int ans=0;
cout<<f[n][m];
}
[=========================]
3、I love sneakers!(变形的分组)
题面:
After months of hard working, Iserlohn finally wins awesome amount of scholarship. As a great zealot of sneakers, he decides to spend all his money on them in a sneaker store.
There are several brands of sneakers that Iserlohn wants to collect, such as Air Jordan and Nike Pro. And each brand has released various products. For the reason that Iserlohn is definitely a sneaker-mania, he desires to buy at least one product for each brand.
Although the fixed price of each product has been labeled, Iserlohn sets values for each of them based on his own tendency. With handsome but limited money, he wants to maximize the total value of the shoes he is going to buy. Obviously, as a collector, he won’t buy the same product twice.
Now, Iserlohn needs you to help him find the best solution of his problem, which means to maximize the total value of the products he can buy.
Input
Input contains multiple test cases. Each test case begins with three integers 1<=N<=100 representing the total number of products, 1 <= M<= 10000 the money Iserlohn gets, and 1<=K<=10 representing the sneaker brands. The following N lines each represents a product with three positive integers 1<=a<=k, b and c, 0<=b,c<100000, meaning the brand’s number it belongs, the labeled price, and the value of this product. Process to End Of File.
Output
For each test case, print an integer which is the maximum total value of the sneakers that Iserlohn purchases. Print "Impossible" if Iserlohn's demands can’t be satisfied.
Sample
Input
5 10000 3
1 4 6
2 5 7
3 4 99
1 55 77
2 44 66
Output
255
•题目大意:
GGrun喜欢各种漂亮的鞋,但他的钱有限,现在将各种鞋按品牌划分(分组),每组里至少选一个,问如果有n种鞋分为k组,你有m元钱,最多能获得的最大价值为多少,如果不能每组至少买一个,那就输出“Impossible”,否则输出最大价值。
•分析:本来的分组背包是每组最多买一个,但是他现在是每组至少选一个,那么我们对每一组进行考虑时,每个物品有两种转移方式:
1:作为本组的第一个物品买进来。
2:不作为本组第一个物品买进来,即前面已经有物品被选过了。
那么我们该如何确定一个物品是否被买过了呢?可以先设一个初始值,在编历时,如果他还是初始值那么就说明该种方案不可行。那么我们设f[i][j]表示第i组体积为j时所能获得的最大价值,初始值设为-1,f[0][0]=0;
状态转移方程:f[i][j]=max(f[i][j],f[i][j-v[k]]+w[k],(f[i][j-v[i]]!=-1),f[i-1][j-v[k]]+w[k],(f[i-1][j-v[k]]!=-1));
附上AC代码,注意多组测试数据:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ps push_back
#define mk make_pair
const int N=1e4+10;
int n,m,k,f[11][N];
struct jj{
int id,cost,v;
};
vector<jj> zu[13];
int main(){
while(cin>>n>>m>>k){
int x,y,z;
for(int i=1;i<=k;i++)zu[i].clear();
for(int i=1;i<=n;i++){
scanf("%d%d%d",&x,&y,&z);
zu[x].ps(jj{x,y,z});
}
memset(f,-1,sizeof(f));
f[0][0]=0;
for(int i=1;i<=k;i++){
for(auto j:zu[i]){
for(int p=m;p>=j.cost;p--){
if(f[i][p-j.cost]!=-1)f[i][p]=max(f[i][p],f[i][p-j.cost]+j.v);
if(f[i-1][p-j.cost]!=-1)f[i][p]=max(f[i][p],f[i-1][p-j.cost]+j.v);
}
}
}
int man=-1;
for(int i=1;i<=m;i++){
man=max(man,f[k][i]);
}
if(man==-1){
cout<<"Impossible"<<endl;
}
else cout<<man<<endl;
}
}
4、Coins(混合背包)
题面:
People in Silverland use coins.They have coins of value A1,A2,A3...An Silverland dollar.One day Tony opened his money-box and found there were some coins.He decided to buy a very nice watch in a nearby shop. He wanted to pay the exact price(without change) and he known the price would not more than m.But he didn't know the exact price of the watch.
You are to write a program which reads n,m,A1,A2,A3...An and C1,C2,C3...Cn corresponding to the number of Tony's coins of value A1,A2,A3...An then calculate how many prices(form 1 to m) Tony can pay use these coins.
Input
The input contains several test cases. The first line of each test case contains two integers n(1<=n<=100),m(m<=100000).The second line contains 2n integers, denoting A1,A2,A3...An,C1,C2,C3...Cn (1<=Ai<=100000,1<=Ci<=1000). The last test case is followed by two zeros.
Output
For each test case output the answer on a single line.
Sample
Input
3 10
1 2 4 2 1 1
2 5
1 4 2 1
0 0
Output
8
4
分析:类似于砝码称重,但是是混合背包,对于0/1背包正常跑就行,对于多重背包用二进制拆分成0/1背包做,对于完全背包就正常跑;
附上AC代码:
点击查看代码
#include <iostream>
#include <cstring>
#define INF 0x3f3f3f3f
using namespace std;
int a[111],c[111];
int n,m;
bool f[111111];
void lingyi(int m,int v) //0-1背包
{//m背包的总容量、v物品的体积
for(int i=m;i>=v;i--)
f[i]|=f[i-v];
}
void wanquan(int m,int v) //完全背包
{//m背包的总容量、v物品的体积
for(int i=v;i<=m;i++)
f[i]|=f[i-v];
}
void GG(int m,int v,int num)//多重背包
{//m背包的总容量、v物品的体积、num物品的数量
if(v*num>=m)
{
wanquan(m,v);
return ;
}
int k=1;
for(k=1;k<=num;k<<=1)
{
lingyi(m,k*v);
num=num-k;
}
if(num)
lingyi(m,num*v);
}
int main()
{
while(cin>>n>>m)
{
if(n==0&&m==0) break;
for(int i=0;i<n;i++)
cin>>a[i];
for(int i=0;i<n;i++)
cin>>c[i];
memset(f,0,sizeof(f));
// f[0]=0;
f[0]=1;
for(int i=0;i<n;i++)
{
GG(m,a[i],c[i]);
}
int sum=0;
for(int i=1;i<=m;i++)sum+=f[i];
cout<<sum<<endl;
}
return 0;
}
THE END
努力之时,莫问前程。