DP求解完全背包问题的优化原理

1. 完全背包问题的形式化描述

  完全背包问题是一类经典的DP(Dynamic Programming,动态规划)问题,问题描述如下:

  有n种重量和价值分别为wivi的物品,从这些物品中挑选出总重量不超过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 × v|(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求解此类问题的优化方法。

猜你喜欢

转载自www.cnblogs.com/sci-dev/p/11907478.html
今日推荐