题目
有N种物品和一个容量为V的背包,每种物品有无限件可以使用。放入第i种物品的费用是Ci,价值是Wi。求解:将哪些物品装入背包,可使这些物品的耗费的费用总和不超过背包的容量,且价值总和最大。
基本思路
从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是取0件、1件、2件......直至取(V/Ci)件等多种策略。
如果仍按照解01背包的思路,令F[i][v]表示前i种物品恰好放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:
F[i][v]=max(F[i-1][v-kCi]+kWi) 0<=kCi<=v
for(int i=1;i<=N;i++)
{
for(int v=0;v<=V;v++)
{
for(int k=0;k*C[i]<=v;k++)
{
F[i][v]=max(F[i][v],F[i-1][v-k*C[i]]+k*W[i]);
}
}
}
printf("%d\n",F[N][V]);
将01背包问题的基本思路加以改进,得到这样一个清晰的方法,但该方法总的复杂度是比较大的,还需要加以优化。
一个简单有效的优化
1.若两件物品i,j满足Ci<=Cj且Wi>=Wj,则完全可以将物品j去掉,不用考虑。
2.若一件物品的费用Ci>V,那么该物品也可以直接去掉,不用考虑
转化为01背包求解
最简单的想法是,考虑到第i种物品最多可以选(V/Ci)件,于是可以把第i种物品转化为(V/Ci)件费用及价值均不变的商品,然后求解这个01背包问题。这样的做法完全没有改进时间复杂度,但这种方法也指明了将完全背包问题转化为01背包问题的思路:
将一种物品拆成多件只能选0件或1件的01背包中的物品
二进制优化
num=1;
for(int i=1;i<=N;i++)
{
for(int j=1;C[i]*j<=V;j*=2)
{
value[num]=j*W[i];
cost[num++]=j*C[i];
}
}
for(int i=1;i<num;i++)
{
for(int j=V;j>=cost[i];j--)
{
dp[j]=max(dp[j],dp[j-cost[i]]+value[i]);
}
}
printf("%d\n",dp[V]);
复杂度为O(VN)的算法
//这个算法使用一维数组
for(int i=1;i<=N;i++)
{
for(int v=C[i];v<=V;v++)
{
F[v]=max(F[v],F[v-C[i]]+Wi);
}
}
你会发现这个代码与01背包的代码只有v的循环次序不同而已。
为什么这个算法就可行呢?首先想想为什么01背包中要按照v递减的次序来循环。让v递减是为了保证第i次循环中的状态F[i][v]是由状态F[i-1][v-Ci]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑"选入第i件物品"这个策略时,依据的时一个绝无已经选入第i件物品的子结果F[i-1][v-Ci]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑"加选一件第i种物品"这种策略时,却正需要一个可能已选入第i件物品的子结果F[i][v-Ci],所以就可以并且必须采用v的递增顺序循环。这就是为何这个简单程序成立的原理。
for(i=1;i<=N;i++)
{
for(v=0;v<=V;v++)
{
if(v>=C[i])
F[i][v]=max(F[i-1][v],F[i][v-C[i]]+W[i]);//注意此处与0-1背包的不同,0-1背包:max(V[i-1][v],F[i-1][v-C[i]+W[i])
else
F[i][v]=F[i-1][v];
}
}
printf("%d\n",F[N][V]);
摘自《背包九讲》