1. 完全背包问题的形式化描述
完全背包问题是一类经典的DP(Dynamic Programming,动态规划)问题,问题描述如下:
有n种重量和价值分别为wi,vi的物品,从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值vi总和的最大值。值得注意的是,每种物品都可以选取无限次。
假设第i种物品选择了ki个,ki∈N,则
目标函数为:
max{Σkivi}
约束条件为:
Σkiwi <= W
2. 完全背包问题的平凡解法
首先对所有物品种类进行1~n的编号。
采用自底向上的递推方法。
设函数F(i,j)表示从编号1~i的种类中选取物品,装入最大允许重量为j的背包中,所能获得的最大总价值。
首先考虑基准情况:选0个物品,即i=0,这时:
F(i,j) = F(0,j) = 0
向上递推。当前从编号1~i的类中挑选物品,产生的最优解取决于从区间1~(i-1)产生的最优解与当前的决策。
要做出决策,需从i中选k个物品(k可以为0),分为如下两种情况:
1。若物品重量wi不超过背包的允许重量j,则可分别选0个、...、k个该物品,kwi都不超过允许重量j。最后再从所有选择结果中挑选最大总价值,作为当前最优解。
F(i,j) = max{ F(i-1,j - k × wi) + k × vi | (k>=0,kwi<=j) }
2。若物品重量wi超过背包的允许重量j,则无法放入当前物品:
F(i,j) = F(i-1,j)
上述两个递推式揭示了当前状态与上一状态之间的转移关系。加上基准情况,我们发现算法已经封闭了,可以编写程序实现。
核心程序
输入:物品种类数n,背包最大允许重量t,第i种物品重量w[i]、价值v[i]。
输出:最大价值dp[n][t]。
初始化:清零dp数组
for(int i=1;i<=n;++i) { for(int j=0;j<=t;++j) { if(j<w[i]) { dp[i][j]=dp[i-1][j]; } else { for(int k=0;k*w[i]<=j;++k) { dp[i][j]=max(dp[i][j], max(dp[i-1][j], dp[i-1][j-k*w[i]]+k*v[i])); } } } }
容易看出渐近复杂度O(n×t×w),为三次复杂度,因此该程序能解决的问题规模非常有限。但这个问题并非不可解决,正如本节标题所暗示的,平凡解法意味着还有较大可优化的空间。
3. 利用数据相关性——时间复杂度优化
观察for(j)的循环与for(k)的循环,猜测两个循环存在数据相关性,例如:F(i-1,j)中选择k个物品的情况,与F(i-1,j-wi)中选择k-1个情况完全相同,推测F(i-1,j)的递推中k>=1的部分在计算F(i-1,j-wi)时就已经求出了,继续k循环会出现重复计算,有可能将k循环合并到j循环中。
为了验证这种想法,推导状态转移方程如下:
题设:
F(i,j) = max{F(i-1,j - k × wi) + k × vi |(k>=0)} ……①
考虑当k==0时,F(i-1,j - k × wi) + k × vi退化为F(i-1,j),所以
F(i,j) = max(F(i-1,j), max{F(i-1,j - k × wi) + k × vi |(k>=1)} )
为了验证之前的猜测,代换k=k+1,等价变形到k>=0的情况:
F(i,j) = max(F(i-1,j), max{F(i-1,j - (k+1) × wi) + (k+1) × vi |(k>=0)})
= max(F(i-1,j), max{F(i-1,(j - wi) - k × wi) + k × vi + vi |(k>=0)} ) ……②
结论仍不明显,将上式②下划线部分提到max{}后面,发现:
F(i,j) = max(F(i-1,j), max{F(i-1,(j - wi) - k × wi) + k × vi |(k>=0)} + vi )
对比①知,上式蓝色部分正等价于F(i, j - wi)。
F(i,j) = max(F(i-1,j), F(i, j - wi) + vi)
这便是最终的表达式,可以看到成功消去了k。
根据推导的状态转移方程编写程序,时间复杂度从三次直接降至二次,我们再次看到了DP的威力。
需要注意,因为F(i-1,j)依赖于F(i-1,j-wi)的计算结果,j必须递增遍历,才能保证计算的正确性。
for(int i=1;i<=n;++i) { for(int j=0;j<=t;++j) { if(j<w[i]) { dp[i][j]=dp[i-1][j]; } else { dp[i][j]=max(dp[i-1][j], dp[i][j-w[i]]+v[i]); } } }
4. 降低dp数组维度——空间复杂度优化
优化前,dp数组的空间复杂度是O(n×t)的。
注意到dp数组中,dp[i]行的各元素值只依赖于前一行dp[i-1],而不依赖于dp[i-2]、……、dp[0]行。这启发我们只存储一行dp数组,然后在该行“原地”更新数据。
仍然有两种情况:
1。对于j<w[i],不必更新dp行。
2。对于其它非1。的情况,需要更新dp行,dp[j]=max(dp[j], dp[j-w[i]]+v[i]); 加粗字体反映了“原地”更新策略。
for(int i=1;i<=n;++i) { for(int j=<w[i];j<=t;++j) { dp[j]=max(dp[j],dp[j-w[i]]+v[i]); } }
因为情况1。的存在,j的取值必须以w[i]为区间起点。
于是空间复杂度降至了O(n))。
5. 结论
首先描述了完全背包问题,然后分析了最直观的trivial解法,由平凡解法发现循环间的数据相关性,并推导出优化后的状态转移方程,推导结论使算法时间复杂度降至O(n×t)。接下来从另一角度——空间复杂度分析dp数组降维优化,使空间复杂度降低至O(n)。综合时空两个方面,得出了DP求解此类问题的优化方法。