1、01背包(每种物品只有一个)
题目 有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。
求解将哪些物 品装入背包可使价值总和最大。
基本思路 这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。 用子问题定义状态:
即表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。 则其状态转移方程便是:
f[i][v] =Max { f[i−1][v] ,f[i−1][v−c[i]] + w[i] }
这个方程非常重要
把第i个物品在考虑加进去的时候,计算一下,加入这个物品后,背包的价值能不能提高,这就是状态方程 的意义。
核心代码:
for (int i=1;i<=n;i++)
for (int v=V;v>=c[i];v--)
f[v]=max(f[v],f[v-c[i]]+w[i]);
代码实现时一般用到三个一维数组:f [V] , v[N] , w[N]。
V表示背包的最大容量,N表示物品个数
数组f存的是状态,比如说,f [ j ] 就表示把背包装到 j 这么重的时候的最大价值
数组v 和 w 分别存价值和重量。
例题1:HDU2602
题意:Teddy捡石头往他的背包里放,给出背包容量,石头个数,还有每个石头的重量和价值。求出怎么样选择 石头,可以获得最大的价值。(经典)
代码:
#include<stdio.h>
#include<string.h>
int max(int a,int b)
{
return a>b?a:b;
}
int main()
{
int T,N,V;
int i,j;
int bag[1010],v[1010],w[1010];
scanf("%d",&T);
while(T--)
{
memset(bag,0,sizeof(bag));//把背包各个状态是的价值设为0
scanf("%d%d",&N,&V);
for(i=0;i<N;i++)
scanf("%d",v+i);
for(i=0;i<N;i++)
scanf("%d",w+i);
for(i=0;i<N;i++) //第i个物品
for(j=V;j>=w[i];j--){ //j状态的背包(计算重量为j是的最大价值)
bag[j]=max(bag[j],bag[j-w[i]]+v[i]);
}
printf("%d\n",bag[V]);
}
return 0;
}
例题2:HDU2546
题意:就是像一种购买方案,在最后一次买的时候,买最贵的把卡的余额欠的最大。但是在买最后一个之前,要把
卡的余额刷到最接近5元(大于等于)。这个过程类似01背包。
思路:该题需要把卡的余额类比成背包重量。需要注意,每种菜的价值和重量相等,都是菜价。也就是背包问题中
v [ i ] == w [ i ]
代码:
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int main()
{
int N,V,m;
int i,j;
int bag[1010],v[1010];
while(~scanf("%d",&N),N)
{
memset(bag,0,sizeof(bag));
for(i=0;i<N;i++)
scanf("%d",v+i);
sort(v,v+N);//通过排序把最贵的菜放到最后一个位置
scanf("%d",&m);//把卡余额m看做背包容量
//饭菜的价格即代表他的价值,也代表他的重量
V=m-5;//我们的目的是把卡余额尽量刷到5元
if(m<5){
printf("%d\n",m);
continue;
}
for(i=0;i<N-1;i++) //第i个菜
for(j=V;j>=v[i];j--){
bag[j]=max(bag[j],bag[j-v[i]]+v[i]);
}
printf("%d\n",m-v[N-1]-bag[V]);//减掉最贵的菜,和规划好的最大消费
}
return 0;
}
总结:状态方程不能死记硬背,要根据题目意思灵活应变。上面的代码中,背包状态都是从V往物品重量递推,且要取等号,恰好装也可以。
2、完全背包(每种物品有无限个)
题目 有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i], 价值是w[i]。求解将哪 些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最 大。
基本思路 这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种 物品的角度考虑,与它相 关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很 多种。如果仍然按照解01背包时 的思路,令f[i][v]表示前i种物品恰放入一个容量为v的背包的最 大价值。可以按照每种物品不同的 策略写出状态转移方程:
f[i][v] = max { f[i−1][ v−k×w[i] ] + k×v[i] } 0 <= k×c[i] <= V
但是实际代码不是这么直接套用的,而是在当前状态下,继续添加当前物品,直到背包装满。
for(int i=0;i<N;i++)
for(int j=w[i];j<=V;j++)//V是背包最大容量
f[j]=max(f[j],f[j-w[i]]+v[i]) //数组w重量,v价值
注意:
和01背包仅有一点区别,就是循环顺序。但是动态规划出来的结果是截然不同的!
先回忆一下01背包为什么要到这循环,因为每一个物品在装进去的时候,都要用到前一个状态的数据来计算当前物品要不要加入。简单说,就是由前面物品装好的状态推出当前物品装入的状态。
而内循环为正序时,先更新状态,再用更新了的状态继续更新当前状态,就产生了一个现象:在不超背包容量的状态下,我可以一直用当前物品不断更新背包状态,即不断往背包里填同一个物品。打个比方,我想在正在装一个重m的物品,进入内循环,我先把f [m] 这个状态更新了,想在f [m]就是装入了m后的最大价值,继续更新f [m+1]的状态,我会用到 f [j]=max( f [j], f [j-v[i]]+v[i]); 这个语句,也就是会调用到更小重量时的状态,而我恰好在这之前更新过了,那不就是我可以第二次把物品m装进背包吗。以此类推,我往后更新状态过程中,我可以无限的把同一个物品 i 往里装。
例题3:HDU1114
题意:某人有个存钱罐(存硬币),空着的时候重E,存满钱是重F。现在他把罐子装满钱了,给出硬币种类和重量,计算存钱罐最少能存多少钱。
思路:完全背包。每种硬币可以无限放。但是这里不是求最大价值,而是求最小价值,所以我们先假设所有状态为正无穷,然后不断更新dp,以求得最小价值。这样在动态规划时需要注意一点,状态0始终未0,即f [ 0 ] =0恒成立
代码:
#include<stdio.h>
#include<algorithm>
using namespace std;
const int INF=0x6fffffff;
int f[10010];
int main()
{
int T,E,F,M,N;
int p,w; //价值和重量
scanf("%d",&T);
while(T--)
{
for(int i=1;i<=10002;i++)f[i]=INF;
f[0]=0;//这一步很重要,给动态规划一个开头
scanf("%d%d%d",&E,&F,&N);
M=F-E; //最大钱数M
for(int i=0;i<N;i++){ //第 i 种硬币
scanf("%d%d",&p,&w);
for(int j=w;j<=M;j++) //不断往里放这种硬币,取钱数少的方案
f[j]=min(f[j],f[j-w]+p);
}
if(f[M]==INF)
puts("This is impossible.");
else
printf("The minimum amount of money in the piggy-bank is %d.\n",f[M]);
}
return 0;
}
总结:与01背包的代码实现类似,但过程却相差甚大,一定要区分代码的计算思路
3、多重背包(每种物品有多个)
与01背包类似,只不过每种物品的数量不止一个。最直接的想法,把物品i有n个想象成,有n个物品一模一样,再用01背包求解即可。
51nod(优化代码)
基准时间限制:1 秒 空间限制:131072 KB 分值: 40 难度:4级算法题
收藏
关注
有N种物品,每种物品的数量为C1,C2......Cn。从中任选若干件放在容量为W的背包里,每种物品的体积为W1,W2......Wn(Wi为整数),与之相对应的价值为P1,P2......Pn(Pi为整数)。求背包能够容纳的最大价值。
Input
第1行,2个整数,N和W中间用空格隔开。N为物品的种类,W为背包的容量。(1 <= N <= 100,1 <= W <= 50000)
第2 - N + 1行,每行3个整数,Wi,Pi和Ci分别是物品体积、价值和数量。(1 <= Wi, Pi <= 10000, 1 <= Ci <= 200)
Output
输出可以容纳的最大价值。
Input示例
3 6
2 2 5
3 3 8
1 4 1
Output示例
9
如Ci = 14,我们可以把它化成如下4个物品:
重量是Wi,体积是Vi
重量是2 * Wi , 体积是2 * Vi
重量是4 * Wi , 体积是4 * Vi
重量是7 * Wi , 体积是7 * Vi
注意最后我们最后我们不能取,重量是8 * Wi , 体积是8 * Vi 因为那样总的个数是1 + 2 + 4 + 8 = 15个了,我们不能多取对吧?
【代码】:
#include<stdio.h>
typedef long long ll;
template<class T>T max(T a,T b)
{
return a>b?a:b;
}
ll f[60000];
ll w[2002000];
ll p[2002000];
int main()
{
int n,W;
scanf("%d%d",&n,&W);
int k=0;
for(int i=0;i<n;i++)
{
int w1,p1,c1;
scanf("%d%d%d",&w1,&p1,&c1);
int t=1;
while(c1>0)
{
if(c1>t)
{
w[k]=t*w1;
p[k]=t*p1;
}
else
{
w[k]=c1*w1;
p[k]=c1*p1;
}
c1=c1-t;
t=t*2;
k++;
}
}
for(int i=0;i<k;i++)
for(int j=W;j>=w[i];j--)
{
f[j]=max(f[j],f[j-w[i]]+p[i]);
}
printf("%lld\n",f[W]);
return 0;
}
例题:HDU 2191
代码:
//HDU 2191 多重背包
#include<stdio.h>
int val[110],wei[110],count[110],dp[110];//价格,重量,袋数。动态背包
int max(int a,int b)
{
return a>b?a:b;
}
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
int n,m;// 钱数,种类
scanf("%d%d",&n,&m);
for(int i=0;i<m;i++)
{
scanf("%d%d%d",val+i,wei+i,count+i);
}
for(int i=0;i<=n;i++) dp[i]=0;//初始为0
for(int i=0;i<m;i++) //第 i 种大米
for(int k=1;k<=count[i];k++) //大米 i 放入的次数
for(int j=n;j>=val[i];j--) // 动态规划
dp[j]=max(dp[j],dp[j-val[i]]+wei[i]);
printf("%d\n",dp[n]); //数组dp存的是重量
}
return 0;
}
4、01背包第k个最优解
思想与01背包相同,不过在动态规划过程中,不可以把前一个状态的最优解扔掉,而要保存下来。
在01背包中,状态数组是 f [ v ] ,表示容积为v时的最优决策。而现在,我不仅要知道最优决策,我还想知道稍微差一点的决策,即第2决策、第3决策....排个名。f [ v ] 我们可以看做是 f [ v ] [ 1 ] 这样的二维数组,他的第二维只有一个元素,也就是最优决策。现在我们增大第二维,比如 f [ v ] [ 3 ] ,意思是,不仅保留了最优解,次解也保留下来了。
也可以理解为,第二维是一个集合,集合里就存了所有可能的决策,并按大小有序,排在第一的就是最优解。
现在问题是 第k个最优决策是多少。
在01背包里,我们只保留了最优解,而把不是最优解的解直接舍弃了,即
f[j]=max(f[j],f[j-w[i]]+v[i])
这时候,较小的那个解直接舍弃了,没保留下来。现在我们要做的就是,借助数组把所有的解都保留下来。
核心代码:
int i,j,t;
for(i=0;i<n;i++) //对每个物品扫描
for(j=v;j>=vol[i];j--) //对每个状态进行更新
{
for(t=1;t<=k;t++)
{ //把所有可能的解都存起来
a[t]=f[j][t];
b[t]=f[j-vol[i]][t]+val[i];
}
int m,x,y;
m=x=y=1;
a[k+1]=b[k+1]=-1;
//下面的循环相当于求a和b并集,也就是所有的可能解
while(m<=k && (a[x]!=-1 || b[y]!=-1))
{
if(a[x]>b[y])
f[j][m]=a[x++];
else
f[j][m]=b[y++];
if(f[j][m]!=f[j][m-1])
m++;
}
}
和01背包的代码比较一下。就是多了里面一层处理比较麻烦,但思想很容易接受。就是把所有可能解先借助a,b两个数组存下来,然后按顺序赋给状态数组f。
例题:HDU2639
模板题
代码:
#include <stdio.h>
#include<string.h>
int f[1010][33];//第一维和普通01背包一样。第二维存多个最优解
int val[110],vol[110];//价值和体积
int a[33],b[33];//用于暂时存储多组最优解
int n,v,k;
int i,j,t,T;
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d%d%d",&n,&v,&k);
for(i=0;i<n;i++) scanf("%d",&val[i]);
for(i=0;i<n;i++) scanf("%d",&vol[i]);
memset(f,0,sizeof(f));//对状态数组清0
for(i=0;i<n;i++) //对每个物品扫描
for(j=v;j>=vol[i];j--) //对每个状态进行更新
{
for(t=1;t<=k;t++)
{ //把所有可能的解都存起来
a[t]=f[j][t];
b[t]=f[j-vol[i]][t]+val[i];
}
int m,x,y;
m=x=y=1;
a[k+1]=b[k+1]=-1;
//下面的循环相当于求a和b并集,也就是所有的可能解
while(m<=k && (a[x]!=-1 || b[y]!=-1))
{
if(a[x]>b[y])
f[j][m]=a[x++];
else
f[j][m]=b[y++];
if(f[j][m]!=f[j][m-1])
m++;
}
}
printf("%d\n",f[v][k]);
}
return 0;
}
5、01背包记录路径.
用二维数组来记录,path[ m ] [ n ] 。其中m表示物品(m<=物品数),n表示背包状态(n<=背包容量)。
比如 path [ i ] [ j ] 表示物品 i 放在了状态 j 的背包中。 前提条件:path数组全部为0,
代码实现记录路径:
for(int i=0;i<n;i++)
for(int j=V;j>=v[i];j--)
if(f[j]<f[j-v[i]]+w[i])
{
f[j]=f[j-v[i]]+w[i];
path[i][j]=1; //把装进去的物品标记一下
}
路径读取代码:
int i=n-1,j=V; //V:背包容量。n个物品
while(i>=0&&j>=0)
{
if(path[i][j])//物品i在j里
{
printf("%d ",i);//把物品i的编号输出
j-=v[i]; //读完了物品i,找下一个背包状态
}
i--;
}