背包
背包问题属于动态规划中的一大类,背包问题的解题思路也基本上离不开动态规划。背包问题的类型多,但掌握每一种背包的核心代码并不难,难的是背包问题的变式。虽说背包问题的类型多,但背包问题的变式更多,变式多,难度大,不多见多做多领悟是学不会背包的。网上关于背包的博客有很多,更有著名的、延用十年至今仍然热门的背包九讲,所以在这里有关背包的博客就不写这么细了,主要是领悟背包的思想,注意一些易错的地方就可以了。
在这里附上pdf版的背包九讲,以供读者学习、参考。
背包九讲.pdf
0-1背包
最基础的背包,有n个物品,第i个物品的重量为w[i],价值为v[i],每个物品只能选0次或1次,现有一容量为bag的背包,问如何规划这n个物品,使其在不超过背包容量的情况下带的价值最多,输出最多价值。
模板代码
for(i=1;i<=n;i++)
for(j=bag;j>=w[i];j--)
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
完全背包
有n个物品,第i个物品的重量为w[i],价值为v[i],每个物品可以选无数次,现有一容量为bag的背包,问如何规划这n个物品,使其在不超过背包容量的情况下带的价值最多,输出最多价值。
模板代码
for(i=1;i<=n;i++)
for(j=w[i];j<=bag;j++)
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
多重背包
有n个物品,第i个物品的重量为w[i],价值为v[i],第i个物品最多选num[i]次,现有一容量为bag的背包,问如何规划这n个物品,使其在不超过背包容量的情况下带的价值最多,输出最多价值。
多重背包的状态转移方程与完全背包的状态转移方程不同,也无法通过归纳状态转移方程达到减少一层循环的目的,所以如果暴力去做多重背包的话时间复杂度是O(n3),这样一来跑1000的数据就会爆,更不用说跑上万的数据了,在这里给大家介绍一种二进制优化法。
我们都知道数据在计算机内存中是以二进制的方式存储的,这也从侧面反映了一个事实,就是2的幂次方的和可以凑出一切正整数,例如1000=1+2+4+8+16+32+64+128+256+489(不够2的整次幂的就写减剩下的数),那么就可以用1,2,4,8,16,32,64,128,256,489凑出1-1000之间任意一个数,例如100=4+32+64。那么就可以将多重背包转化一下思路,例如一件物品最多可以取1000件,那么就可以将这1000件分为1件,2件,4件,8件,16件,32件,64件,128件,256件,489件,把每一份当作0-1背包来处理。例如如果这1000件取0件最优,那么这几份就全不取;如果这1000件取100件最优,那么就取4件、32件、64件这3份;如果这1000件取1000件最优,那么这几份就全取。这样一来,我们把要跑1001次的循环(枚举取0-1000件)压缩成了要跑10次的循环(枚举分的每一份取还是不取),且一件物品能取的数目越多,优化越明显,达到了优化时间的目的。
模板代码
for(i=1;i<=n;i++)
{
for(k=1;num[i]>0;k*=2)
{
number=min(k,num[i]);
for(j=bag;j>=w[i]*number;j--)
dp[j]=max(dp[j],dp[j-w[i]*number]+v[i]*number);
num[i]-=number;
}
}
混合背包
即0-1背包、完全背包、多重背包的混合问题。
在这里我们假设第i件物品可取的数量为num[i]。
当num[i]=-1时表示这件物品可以取1次,为0-1背包。
当num[i]=0时表示这件物品可以取无数次,为完全背包。
当num[i]>0时表示这件物品可以取num[i]次,为多重背包。
模板代码
for(i=1;i<=n;i++)
{
if(num[i]==-1)
{
for(j=bag;j>=w[i];j--)
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
else if(num[i]==0)
{
for(j=w[i];j<=bag;j++)
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
else
{
for(k=1;num[i]>0;k*=2)
{
number=min(k,num[i]);
for(j=bag;j>=w[i]*number;j--)
dp[j]=max(dp[j],dp[j-w[i]*number]+v[i]*number);
num[i]-=number;
}
}
}
二维背包
二维背包
同0-1背包,只不过物品的重量变成了两个,背包的容量变成了两个。
有n个物品,第i个物品的重量1为w1[i],重量2为w2[i],价值为v[i],每个物品只能选0次或1次,现有一容量1为bag1,容量2为bag2的背包,问如何规划这n个物品,使其在不超过背包容量的情况下带的价值最多,输出最多价值。
二维背包的做法是由0-1背包的做法拓展而来。
for(i=1;i<=n;i++)
{
for(j=bag1;j>=w1[i];j--)
for(k=bag2;k>=w2[i];k--)
dp[j][k]=max(dp[j][k],dp[j-w1[i]][k-w2[i]]+v[i]);
}
多维背包
同二维背包,只不过物品的重量变成了多个,背包的容量变成了多个。
是几维背包就开几维数组,跑几层内循环,一般不会太多,因为时间、空间复杂度太大。
有限制条件的背包
分组背包
分组背包问题
先放个例题,解法有待思考……
依赖背包
有依赖的背包问题
先放个例题,解法有待思考……
注意
与0-1背包相关的背包跑内循环时是倒着跑的,即:
for(j=bag;j>=w[i];j--)
而其它的背包跑内循环时是正着跑的,即:
for(j=w[i];j<=bag;j++)
做背包题时,一定要搞清楚dp数组记录的是什么,以及dp[i](一维例子)、dp[i][j](二维例子)、dp[i][j][k](三维例子)代表着什么。
dp数组在计算前一般要赋初值,让求最大价值赋初值时赋成0,比较时用max函数;让求最小价值赋初值时赋成正无穷,比较时用min函数。具体情况具体分析,并不是所有题目都赋0或正无穷,尤其是像dp[0]这种特殊的下标一定要注意要不要赋特殊值。
在求背包能不能装满时,如果dp[bag]在计算完成后仍与赋的初值相同,则不能装满。
例题
#include <stdio.h>
#include <string.h>
int w[1005],v[1005],dp[1005];
int max(int x,int y)
{
return x>=y?x:y;
}
int main()
{
int i,j,t,n,bag;
scanf("%d",&t);
while(t--)
{
memset(dp,0,sizeof(dp));
scanf("%d%d",&n,&bag);
for(i=1;i<=n;i++)
scanf("%d",&v[i]);
for(i=1;i<=n;i++)
scanf("%d",&w[i]);
for(i=1;i<=n;i++)
for(j=bag;j>=w[i];j--)
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
printf("%d\n",dp[bag]);
}
return 0;
}
#include <stdio.h>
#include <algorithm>
using namespace std;
long long n,bag,ans;
struct jgt
{
int w,v;
double r;
}herb[105];
int cmp(struct jgt a,struct jgt b)
{
return a.r<b.r;
}
void search(long long count,long long sumw,long long sumv)
{
long long num;
if(sumv>ans)
ans=sumv;
if(count>n)
return ;
if(count+1<=n)
{
num=(bag-sumw)/herb[count+1].w+1;
if(sumv+num*herb[count+1].v<ans)
return ;
}
if(sumw+herb[count].w<=bag)
search(count+1,sumw+herb[count].w,sumv+herb[count].v);
search(count+1,sumw,sumv);
}
int main()
{
int i;
while(scanf("%lld%lld",&n,&bag)!=EOF)
{
for(i=1;i<=n;i++)
{
scanf("%d%d",&herb[i].w,&herb[i].v);
herb[i].r=(double)herb[i].w/herb[i].v;
}
sort(herb+1,herb+1+n,cmp);
ans=0;
search(1,0,0);
printf("%lld\n",ans);
}
return 0;
}
乍一看这题是道简单的0-1背包,再一看我们会发现这题包太大了,背包容量最大可达1e9,开一维包都不可能,题目难度陡然上升。那这题该怎么做呢?这题也是我问了师哥后结合着网上的码才过的。
首先呢,我们用一个结构体来记录与每一件物品有关的数据,也就是重量、价值和性价比,然后我们按照性价比由高到低sort一下,然后从第一件物品开始深搜,search函数的3个参数分别代表当前搜索物品的编号,总重量和总价值。每当总价值高于最优解时就更新最优解,这不难理解,在这里需要注意这两个返回条件。
第一个返回条件就是如果当前搜索物品已经超过n了就要返回,因为没有物品可搜了啊。第二个返回条件就是如果当前总价值加上背包当前剩下的空间全部装下一个物品(向上取整以保证贪心)所带来的收益还是小于最优解(不能等于最优解以保证贪心),那么就要返回,至于这其中的道理我还是不太懂,只能隐隐约约感觉到这类似于贪到极致都没之前贪就不用费这功夫了。
然后就是关于当前物品选与不选的问题,如果当前状态还能装下就装,不能装下就不装。你可以认为我们先贪值钱的后贪不值钱的,只不过如果当前状态装不下这件物品了我们就要忍痛割爱,否则就要贪到底。
#include <stdio.h>
#include <string.h>
int dp[1000005];
int Max(int x,int y)
{
return x>=y?x:y;
}
int main()
{
int i,j,k,t,n,bag,max,tmax,w[55];
scanf("%d",&t);
for(i=1;i<=t;i++)
{
max=0,tmax=0;
memset(dp,-1,sizeof(dp));
dp[0]=0;
scanf("%d%d",&n,&bag);
bag--;
for(j=1;j<=n;j++)
scanf("%d",&w[j]);
for(j=1;j<=n;j++)
for(k=bag;k>=w[j];k--)
dp[k]=Max(dp[k],dp[k-w[j]]+1);
for(j=bag;j>=1;j--)
if(dp[j]>max)
max=dp[j],tmax=j;
printf("Case %d: %d %d\n",i,max+1,tmax+678);
}
return 0;
}
这题dp数组记录的是最多听歌数,且听歌时长不能正好等于包的大小,否则不是最优解,包的容量应该减一。
dp数组赋初值时应该赋为-1,这样得到的听歌数才是正确的。由这题给的样例可以看出听歌数的优先级高于听歌时长,所以在查找结果时应该从bag到1遍历,找出听歌数最多且听歌时长最长的那一个,最后的结果别忘了分别加上1和678,因为有劲(jing四声)歌金曲啊(没听过,好想听一听啊)。
#include <stdio.h>
#include <string.h>
int w[105],v[105],dp[100005];
int max(int x,int y)
{
return x>=y?x:y;
}
int main()
{
int i,j,n,bag;
while(scanf("%d",&n)!=EOF)
{
memset(dp,0,sizeof(dp));
for(i=1;i<=n;i++)
scanf("%d%d",&v[i],&w[i]);
scanf("%d",&bag);
for(i=1;i<=n;i++)
for(j=w[i];j<=bag;j++)
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
printf("%d\n",dp[bag]);
}
return 0;
}
#include <stdio.h>
int dp[32769]={1};
int main()
{
int i,j,n;
for(i=1;i<=3;i++)
for(j=i;j<=32768;j++)
dp[j]+=dp[j-i];
while(scanf("%d",&n)!=EOF)
printf("%d\n",dp[n]);
return 0;
}
这题是道动态规划的题,要注意那两层循环不要反着写,也就是这样:
for(i=1;i<=32768;i++)
for(j=1;j<=3;j++)
dp[i]+=dp[i-j];
//只是举例当然不能这么写,下标都越界了……
这样写的话会发现答案出奇的大,因为这相当于斐波那契数列了啊(其实比斐波那契数列还大得多)。这是为什么呢?因为这里面有很多重复啊,这么写的意思相当于10元的组成方案是由7元(加3组成10)的方案加8元的方案(加2组成10)加9元的方案(加1组成10)组成,其实不然,这里面有很多重复的情况。例如某种7元的组成方案是1 1 1 1 3,加3后变为1 1 1 1 3 3,某种9元的组成方案是1 1 1 3 3,加1后变为1 1 1 1 3 3,两种情况完全相同,被多算了,且数越大被多算的次数就越多。正确的做法是像上面的代码那样跑这两层循环,这么跑保证了在加1元的时候组成情况中不含1元(什么都不含),在加2元的时候组成情况中不含2元(含1元),在加3元的时候组成情况中不含3元(含1元、2元),这样得到的答案才是正确的。
#include <stdio.h>
#include <string.h>
int w[505],v[505],dp[10005];
int min(int x,int y)
{
return x<=y?x:y;
}
int main()
{
int i,j,t,bag_min,bag_max,bag,n;
scanf("%d",&t);
while(t--)
{
memset(dp,0x3f,sizeof(dp));
dp[0]=0;
scanf("%d%d",&bag_min,&bag_max);
bag=bag_max-bag_min;
scanf("%d",&n);
for(i=1;i<=n;i++)
scanf("%d%d",&v[i],&w[i]);
for(i=1;i<=n;i++)
for(j=w[i];j<=bag;j++)
dp[j]=min(dp[j],dp[j-w[i]]+v[i]);
if(dp[bag]==0x3f3f3f3f)
printf("This is impossible.\n");
else
printf("The minimum amount of money in the piggy-bank is %d.\n",dp[bag]);
}
return 0;
}
这题让求最小价值,那么在给dp数组赋初值的时候就该赋成正无穷。
让求最大价值赋初值时赋成0,比较时用max函数。
让求最小价值赋初值时赋成正无穷,比较时用min函数。
注意这里题目给的不是包的大小,而是空包的重量和装满时背包的重量,后者减前者才是包的大小。如果在最后发现dp[bag]仍为正无穷,说明这个背包无法被装满,也就不符合题目要求,输出impossible。
#include <stdio.h>
#include <string.h>
int num[10],dp[500000];
int main()
{
int i,j,k,t,sum,count,bag;
for(t=1;;t++)
{
sum=0;
memset(num,0,sizeof(num));
memset(dp,0,sizeof(dp));
dp[0]=1;
for(i=1;i<=6;i++)
{
scanf("%d",&num[i]);
sum+=i*num[i];
}
if(sum==0)
break;
if(sum%2)
{
printf("Collection #%d:\n",t);
printf("Can't be divided.\n\n");
continue;
}
else
{
bag=sum/2;
for(i=1;i<=6;i++)
{
if(num[i])
{
for(j=1;j<=num[i];j*=2)
{
count=i*j;
for(k=bag;k>=count;k--)
if(dp[k-count])
dp[k]=1;
num[i]-=j;
}
if(num[i])
{
count=i*num[i];
for(k=bag;k>=count;k--)
if(dp[k-count])
dp[k]=1;
}
}
}
if(dp[bag])
{
printf("Collection #%d:\n",t);
printf("Can be divided.\n\n");
}
else
{
printf("Collection #%d:\n",t);
printf("Can't be divided.\n\n");
}
}
}
return 0;
}
这题是一道多重背包题,用到了二进制优化。题目问你能不能平分,其实就是把一个大包分成两个小包,问你小包能不能装满。如果物品总重量为奇数则肯定不能平分,直接输出impossible。
这题的dp数组只存0和1,0代表当前容量不可被装满,1代表当前容量可被装满,然后用二进制优化跑就行了,如果之前状态是0那么现在状态也是0,如果之前状态是1那么现在状态也是1,最后判断dp[bag](这里的bag是小包的容量)是0是1即可。
#include <stdio.h>
#include <string.h>
int w[105],v[105],dp[105][105];
int max(int x,int y)
{
return x>=y?x:y;
}
int main()
{
int i,j,l,n,m,k,s,ans;
while(scanf("%d%d%d%d",&n,&m,&k,&s)!=EOF)
{
memset(dp,0,sizeof(dp));
for(i=1;i<=k;i++)
scanf("%d%d",&v[i],&w[i]);
for(i=1;i<=k;i++)
for(j=w[i];j<=m;j++)
for(l=1;l<=s;l++)
dp[j][l]=max(dp[j][l],dp[j-w[i]][l-1]+v[i]);
ans=-1;
for(i=1;i<=m;i++)
{
if(dp[i][s]>=n)
{
ans=m-i;
break;
}
}
printf("%d\n",ans);
}
return 0;
}
这题是一道二维背包题,有忍耐度和杀怪数两个花费,开二维数组。
最后在查找结果时一定要从1遍历到m找第一次出现dp[i][s]大于m的i,m-i即为保留的最大忍耐度。ans赋初值时赋为-1,如果遍历完也没找到dp[i][s]大于m的i输出的ans即为-1。在这里一定要遍历dp[i][s],因为只有杀怪数最多(即为s)时,所获得的经验才为最多,用到了贪心思想。
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
int dp[5005];
struct jgt
{
int p,q,v;
}thing[505];
int max(int x,int y)
{
return x>=y?x:y;
}
int cmp(struct jgt a,struct jgt b)
{
return a.q-a.p<b.q-b.p;
}
int main()
{
int i,j,n,bag;
while(scanf("%d%d",&n,&bag)!=EOF)
{
memset(dp,0,sizeof(dp));
for(i=1;i<=n;i++)
scanf("%d%d%d",&thing[i].p,&thing[i].q,&thing[i].v);
sort(thing+1,thing+1+n,cmp);
for(i=1;i<=n;i++)
for(j=bag;j>=thing[i].q;j--)
dp[j]=max(dp[j],dp[j-thing[i].p]+thing[i].v);
printf("%d\n",dp[bag]);
}
return 0;
}
这题是一道0-1背包的变式题,做法就是将每一件物品前提金与实际金的差值由小到大sort一下,然后再按0-1背包去做即可。
注意内层循环到第i件物品的前提金即停止,而dp数组还是减去第i件物品的实际金。
由此可见背包问题的变式是真的多,想要遇见背包题就会做只有两种可能,一是你的编程思维活跃,见到什么题都能很快想到它的解法;二是多见多做,用努力弥补不足。