今天,我们来讲一讲背包问题中的另一个经典题型——完全背包。
问题化简
有N种物品和一个容量为V的背包。第i种物品的费用是c[i],价值是w[i],每种物品的数量可以任选。求解将哪些物品装入背包可使总价值最大。
一个解法
注意到完全背包和01背包的不同之处在于:完全背包每种物品可以无限取,而01背包每种物品只能取一次。
有了01背包的基础,我们定义f[i][j]表示前i种物品,占用了j的空间,所能得到的最大价值。转移的时候,由于每种物品可以取多次,所以我们要枚举当前物品要取多少次,而不是枚举取或不取。
通过分析,转移方程很容易就能得到了:
f[i][j]=max{f[i-1][j-k*c[i]]+k*w[i] | 0<=k*c[i]<=j}
这样求解的时间复杂度最坏情况下是O(NV^2)(每种物品的体积都是1),空间复杂度是O(NV)。
核心代码如下:
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= v; ++j)
for (int k = 0; k * c[i] <= j; ++k)
f[i][j] = max(f[i][j], f[i - 1][j - k * c[i]] + k * w[i]);
int ans = 0;
for (int i = 1; i <= v; ++i)
ans = max(ans, f[n][i]);
那么,是否还有优化的余地呢?
正解驾到
我们类比01背包的优化方法,f[i][j]的值只和f[i-1][0...j]有关,那么很显然可以先把空间复杂度降到O(V)。
时间复杂度应该怎么优化呢?
观察代码可以发现,第三重循环非常令人讨厌(手动滑稽)。我们要想个办法把这重循环优化掉。这里还是要类比到01背包。
先贴上01背包优化后的程序:
for (int i = 1; i <= N; ++i)
for (int j = V; j >= 0; --j)
if (j >= c[i])
f[j] = max(f[j], f[j - c[i]] + w[i]);
int ans = 0;
for (int j = 0; j <= V; ++j)
ans = max(ans, f[j]);
01背包程序中的第二重循环到倒过来是因为f[j+1]处理后不会影响到f[j]的转移。这么处理是因为计算f[j]的值的时候,f[1...j-1]还没算上第i件物品的贡献。换句话说,就是防止了一件物品被取多次。
等等,完全背包不就是要求一个物品可以取多次吗?
所以,我们只要把01背包的第二重循环正过来就能正确解决完全背包的问题了。
代码如下:
for (int i = 1; i <= N; ++i)
for (int j = 0; j <= V; ++j)
if (j >= c[i])
f[j] = max(f[j], f[j - c[i]] + w[i]);
int ans = 0;
for (int j = 0; j <= V; ++j)
ans = max(ans, f[j]);
总结,完全背包的实现和01背包的实现是相似的,要注意区分。
最后,动态规划最重要的不是代码,而是思想!!!