dp[i][v]=max{dp[i-1][v-k*vol[i]]+k*val[i] | 0<=k*vol[i]<=v}
这跟01背包问题一样有O(VN)个状态需要求解,但求解每个状态的时间已经不是常数了,求解状态dp[i][v]的时间是O(v/vol[i]),总的复杂度可以认为是O(V*Σ(V/vol[i])),是比较大的。
将01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明01背包问题的方程的确是很重要,可以推及其它类型的背包问题。但我们还是试图改进这个复杂度。
一个简单有效的优化:
完全背包问题有一个很简单有效的优化,是这样的:若两件物品i、j满足vol[i]<=vol[j]且val[i]>=val[j],则将物品j去掉,不用考虑。这个优化的正确性显然:任何情况下都可将价值小费用高得j换成物美价廉的i,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。
这个优化可以简单的O(N^2)地实现,一般都可以承受。另外,针对背包问题而言,比较不错的一种方法是:首先将费用大于V的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以O(V+N)地完成这个优化。这个不太重要的过程就不给出伪代码了,希望你能独立思考写出伪代码或程序。
转化为01背包问题求解:
更高效的转化方法是:把第i种物品拆成费用为vol[i]*2^k、价值为val[i]*2^k的若干件物品,其中k满足vol[i]*2^k<=V。这是二进制的思想,因为不管最优策略选几件第i种物品,总可以表示成若干个2^k件物品的和。这样把每种物品拆成O(log V/vol[i])件物品,是一个很大的改进。
但我们有更优的O(VN)的算法。
O(VN)的算法:
for(i=1;i<=N;i++)
for(v=0;v<=V;v++)
dp[v]=max{dp[v],dp[v-vol]+val};
你会发现,这个伪代码与01背包的伪代码只有v的循环次序不同而已。为什么这样一改就可行呢?首先想想为什么01背包中要按照v=V~0的逆序来循环。这是因为要保证第i次循环中的状态dp[i][v]是由状态dp[i-1][v-vol[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果dp[i-1][v-vol[i]]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果dp[i][v-vol[i]],所以就可以并且必须采用v=0~V的顺序循环。这就是这个简单的程序为何成立的道理。
值得一提的是,上面的伪代码中两层for循环的次序可以颠倒。这个结论有可能会带来算法时间常数上的优化。
这个算法也可以以另外的思路得出。例如,将基本思路中求解dp[i][v-vol[i]]的状态转移方程显式地写出来,代入原方程中,会发现该方程可以等价地变形成这种形式:
dp[i][v]=max{dp[i-1][v],dp[i][v-vol[i]]+val[i]}
将这个方程用一维数组实现,便得到了上面的伪代码。
最后抽象出处理一件完全背包类物品的过程伪代码:
void CompletePack(int val,int vol)
{
for v=vol;v<=V;v++)
dp[v]=max{dp[v],dp[v-vol]+val};
}
//完全背包模板
#include<cstdio>
int dp[10000],V;
inline int max(int a,int b)
{
return a>b ? a : b;
}
void CompletePack(int val,int vol)
{
for(int v=vol;v<=V;v++)
dp[v]=max(dp[v],dp[v-vol]+val);
}
int main()
{
int n,val[1000],vol[1000];
scanf("%d%d",&n,&V);
// 两种不同的初始化方式:
//memset(dp,0,sizeof(dp)); // 只希望价格尽量大
//memset(dp,-1,sizeof(dp)),dp[0]=0; // 要求恰好装满背包
for(int i=0;i<n;i++)
scanf("%d%d",&val[i],&vol[i]);
for(int i=0;i<n;i++)
CompletePack(val[i],vol[i]);
printf("%d\n",dp[V]);
return 0;
}