1. 完全背包问题的形式化描述
完全背包问题是一类经典的DP(Dynamic Programming,动态规划)问题,问题描述如下:
有n种重量和价值分别为wi,vi的物品,从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值vi总和的最大值。值得注意的是,每种物品都可以选取无限次。
假设第i种物品选择了ki个,ki∈N,则
目标函数为:
max{Σkivi}
约束条件为:
$ \sum k_i w_i \leq W $
2. 完全背包问题的平凡解法
首先对所有物品种类进行1~n的编号。
设函数F(i,j),其意义为:从编号1~i的种类中选取物品,装入最大允许重量为j的背包中,所能获得的最大总价值是F(i,j)。
首先考虑基准情况:选0个物品,即i=0,这时:
F(i,j) = F(0,j) = 0
对于子问题F(i,j)的求解,其最优解的决策依赖于涉及1~(i-1)类物品的子问题F(i-1,j)。
当前要做出决策,需选k个i类物品(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
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. 利用数据相关性——时间复杂度优化
利用循环间数据相关性优化算法是一种常见的技巧。例如“最大子段和”问题的蛮力解法中,利用数据相关性可以将O(n^3)的复杂度直接优化为O(n^2)。同理,在求Σnx=1 x!(连续阶乘和)问题中,利用数据相关性可将O(n^2)直接优化为O(n)。这些例子展现了该技巧的生命力。
回到我们的DP解法,观察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。
根据推导的状态转移方程编写程序,时间复杂度直接从三次降至了二次。
需要注意,因为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
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
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]为区间起点。
值得注意的是,j循环按递增顺序枚举,这与我们在第3节中得出的结论一致。
经过优化,空间复杂度降至了O(n))。
5. 结论
首先描述了完全背包问题,然后分析了最直观的trivial解法,由平凡解法发现循环间的数据相关性,推导出优化后的状态转移方程,推导结论使算法时间复杂度降至O(n×t)。接下来从另一角度——空间复杂度分析dp数组降维优化,使空间复杂度降低至O(n)。综合时空两个方面,得出了DP求解此类问题的优化方法。