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

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

  有n种重量和价值分别为wipi的物品,$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 × p|(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)。综合时空两个方面,得出了优化方法。

原文地址:https://www.cnblogs.com/cassuto/p/11907478.html