20201013 day34 复习8:动态规划之背包变式

LIS&LCS

最长上升子序列(LIS)

POJ2533

题意显然。
子序列:序列\(\{a_n\}\),则其一个子序列是存在一些数\(j_x\),使得\(1\le j_1<j_2<...\le n\),构成子序列\(\{a_j\}\)

题解

\(\text{O}(n^2)\)

\(f_i\)表示以\(a_i\)结尾的上升子序列最长为多少。初始化显然每个\(f都为1\)。我们从\(1\)~\(i-1\)枚举\(f_j\),当\(a_j<a_i\)时,\(f_i=max(f_i,f_j+1)\)
最后我们输出最大的\(f_i\)即可。

\(O(n\log n)\)

设原数组为\(a_n\),设\(f_i\)表示长度为\(i\)的序列中最长上升的最小值(他是原数组的数值)

如果a[i]>dp[len],len表示当前LIS最长长度,就直接放入末尾
否则,在dp数组中寻找第一个大于等于a[i]的位置,把他替换成a[i],这样的话dp[len]的数值变得稍小,可以更好地在后面接上升序列

for(int i=1; i<=n; i++){
        if(a[i]>dp[len]) dp[++len]=a[i];
        else{
            dp[lower_bound(dp+1,dp+1+len,a[i])-dp]=a[i];
        }
    }

最长公共子序列(LCS)

01背包(一些优化)

空间复杂度优化

二维的01背包很好写。时间复杂度和空间复杂度均为\(O(NV)\),而空间复杂度则可以优化到\(O(V)\)
我们考虑二维数组\(F(i,0...V)\)优化到\(F(0...V)\),我们一定会需要一个主循环i-->N,而我们考虑,\(F(i,v)\)是由\(F(i-1,v)\)\(F(i-1,v-C_i)\)两个子问题递推而来,能否保证在推\(F(i,v)\)时(也就是在第\(i\)次主循环时)能够取用\(F(i-1,v),F(i-1,v-C_i)\)的值?事实上,这要求在每次主循环中我们以\(v=V...0\)的递减顺序计算\(F(v)\),这样才能保证\(F(v)\)\(F(v-C_i)\)保存的是状态\(F(i-1,v-C_i)\)的值。

for i=1 to N
      for v=V to C[i]
            F[v]=max{F[v],F[v-C[i]]+W[i]};

其中的最后一句,对应原来的转移方程,因为现在的\(F(v-C_i)\)就相当于原来的\(F(i-1,v-C_i)\),如果改成顺序,就变成由\(F(i,v)\)\(F(i,v-C_i)\)推导得到。

恰好装满

仅仅是在初始化的时候,普通的背包都初始化为0,而恰好装满要初始化为\(-∞\).

常数优化

第二重循环的下限可以改进。它可以被优化为:
for(int \(v=V\) to \(\max(\sum\limits_i^NW_i,C_i)\)

完全背包(一些优化)

一个简单有效的优化

根据性价比去掉某些物品。总的来说,\(C_i\le C_j,W_i\ge W_j\),此时就可以去掉\(j\)物品。这个优化可以\(O(N^2)\)解决。
另一种优化是:把\(V_i\ge V\)的物品山区,然后利用类似计数排序的原理,计算出相同费用中价值最高的是哪个,可以\(O(V+N)\)完成优化。

转化成01背包

考虑到第\(i\)种物品最多选\(\lfloor\dfrac{V}{C_i}\rfloor\),于是可以把第\(i\)种物品转化为\(\lfloor\dfrac{V}{C_i}\rfloor\)件费用和价值均不变的01背包。这个做法丝毫没有改进时间复杂度,但是指明了转化的思路。
另一种更高效的转化方法是:把第\(i\)种物品拆分成费用为\(C_i2^k\),价值为\(W_i2^k\)的若干件物品,其中\(k\in\left[0,\log_2\left( \dfrac{V}{C_i} \right)\right]\),即\(C_i2^k\le V\)的非负整数\(k\)
这是二进制的思想,因为不管最优策略选几件第\(i\)种物品,其件数携程二进制后,总能表示称若干个\(2^k\)件物品的和,这样就把每种物品拆成\(O\left(\log\dfrac{V}{C_i}\right)\),是一个很大的改进。

\(O(VN)\)的算法

      F[0..V ] = 0
      for i = 1 to N
            for v = Ci to V
                  F[v] = max(F[v], F[v − Ci] + Wi)

你会发现,这个伪代码与01背包问题的伪代码只有v的循环次序不同而已。
为什么这个算法就可行呢?首先想想为什么01背包中要按照v递减的次序来循环。让v递减是为了保证第i次循环中的状态\(F[i, v]\)是由状态\(F[i − 1, v − C_i]\)递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果\(F[i −1, v − C_i]\)。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果\(F[i, v − C_i]\),所以就可以并且必须采用v递增的顺序循环。这就是这个简单的程序为何成立的道理。
值得一提的是,上面的伪代码中两层for循环的次序可以颠倒。这个结论有可能会带来算法时间常数上的优化。
这个算法也可以由另外的思路得出。例如,将基本思路中求解\(F[i, v − C_i]\)的状态转移方程显式地写出来,代入原方程中,会发现该方程可以等价地变形成这种形式:\(F[i, v] = \max(F[i − 1, v], F[i, v − C_i] + W_i)\)
将这个方程用一维数组实现,便得到了上面的伪代码。
最后抽象出处理一件完全背包类物品的过程伪代码:

def CompletePack(F, C, W)
      for v = C to V
            F[v] = max{F[v], f[v − C] + W}

多重背包

分组背包

有依赖的背包

二维费用背包

泛化物品背包

其他变式

posted @ 2020-10-13 19:02  刘子闻  阅读(88)  评论(0编辑  收藏  举报