1. 完全背包问题的形式化描述
有n种重量和价值分别为wi,pi的物品,$i=1,2,...,n$,从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和的最大值。值得注意的是,每种物品都可以选取无限次。
形式化描述为:设第i种物品选择了ki个,ki∈N,则问题即为求$ k_1,k_2,...,k_n $,满足:
目标函数:
egin{equation*} max{sum_{i=1}^{n}k_i p_i} end{equation*}
约束条件:
egin{equation*} left{ egin{aligned} sum_{i=1}^{n} k_i w_i leq W \ k_i in N end{aligned} ight. end{equation*}
可以证明完全背包问题具有最优子结构性质,所以可以采用DP(Dynamic Programming)求解。
2. 完全背包问题的trivial解法
Trivial解法较为直观,因此首先介绍。但这种解法效率很低,后文将给出优化过程。
设函数F(i,j)表示在重量限制j的条件下将前i种物品选择性装入背包获得的最大总价值。
首先考虑基准(边界条件):选0个物品,即i=0,这时显然有
F(0,j) = 0
对于子问题F(i,j),其最优解依赖于前面子问题的最优解:
在物品重量kwi不超过背包的重量限制j的前提下,分别选0,1,2,...,k个该物品。最后再从所有选择结果中挑选最大总价值,作为当前最优解,则状态转移方程为:
F(i,j) = max{ F(i-1,j - k × wi) + k × pi | (k>=0,kwi<=j) }
这是因为,对于第i种物品,当装入k件物品wi后,背包的价值增加了k × pi,但剩余重量减少了k × wi。注意这只是第i种物品,剩下的其它物品要在前i-1种中选取,因此问题规约为在重量限制j - k × wi的条件下,将前i-1种物品选择性地装入背包所获得的最大总价值,即F(i-1,j - k × wi),可见这是一个与原问题性质完全相同的子问题。
加之基准情况,算法过程已经封闭,其实现如下
核心程序
输入:物品种类数n,背包最大允许重量t,第i种物品重量w[i]、价值p[i]。
输出:最大价值dp[n][t]。
初始化:清零dp数组
for(int i=1;i<=n;++i) { for(int j=0;j<=t;++j) { 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*p[i])); } } }
分析可知,在最坏情况下,时间复杂度$ O(nt^2) $。但正如标题所说的,trivial解法还有可优化的空间。
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变量的循环?为了验证这种想法,推导如下:
假设
F(i,j) = max{F(i-1,j - k × wi) + k × pi |(k>=0)} ……①
考虑当k==0时,F(i-1,j - k × wi) + k × pi等价于F(i-1,j),所以
F(i,j) = max(F(i-1,j), max{F(i-1,j - k × wi) + k × pi |(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) × pi |(k>=0)})
= max(F(i-1,j), max{F(i-1,(j - wi) - k × wi) + k × pi + pi |(k>=0)} ) ……②
结论仍不明显,将上式②下划线部分提到max{}后面,发现:
F(i,j) = max(F(i-1,j), max{F(i-1,(j - wi) - k × wi) + k × pi |(k>=0)} + vi )
对比①知,上式蓝色部分恰等价于F(i, j - wi),这基本验证了我们的想法。
F(i,j) = max(F(i-1,j), F(i, j - wi) + pi)
这便是最终的状态转移方程,可以看到成功消去了k。最终的方程也非常直观: 对于第i种物品,当装入1件物品wi后,背包的价值增加了pi,但剩余重量减少了wi。注意剩下的其它物品仍然要在前i种中选取,即求解F(i, j - wi)。请对比F(i, j - wi)和trivial解法中的F(i-1, j - k × wi)以帮助理解。
在该算法下,时间复杂度降至O(n×t)。
值得注意的是,因为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]]+p[i]); } } }
4. 降低dp数组维度——空间复杂度优化
在优化的解法中,空间复杂度为O(n×t)。
注意到dp[i]一行的各元素值只依赖于前一行dp[i-1],而不依赖于dp[i-2]、……、dp[0]行。这启发我们只存储一行dp数组,然后在该行原地更新。
for(int i=1;i<=n;++i) { for(int j=w[i];j<=t;++j) { dp[j]=max(dp[j],dp[j-w[i]]+p[i]); } }
经过优化,空间复杂度降至了O(n)。
5. 结论
首先描述了完全背包问题,然后分析了最直观的trivial解法,由trivial解法发现循环间的数据相关性,推导出优化后的状态转移方程,推导结论使算法时间复杂度降至O(n×t)。
接下来从另一角度——空间复杂度优化,使空间复杂度降低至O(n)。综合时空两个方面,得出了优化方法。