0%

速通斜率优化DP

前几天在写HDU6619的时候,问题最终转化成了一个形如下式的DP:

这个式子很简单,也就是当前状态的值由前面某个状态的值加上两个状态之间的数组值的和得到,且当前状态的值要尽可能大。按照式子直接枚举当前状态的上一状态的话,复杂度是$O(n^2)$。但本题的$n$是$10^5$,直接写肯定会T掉。我一时半会没有想出优化的方法,看了题解才想起来,这个DP可以用斜率优化来加速转移。于是,我准备快速复习一下斜率优化的用法。

斜率优化DP用法示例

还是回到刚刚那个式子:

右边一堆求和实在是太碍眼了。我们可以先对数组求前缀和,这样求和直接被前缀和做差代替:

如果没有那个$(n - i)$就好了。这样上一状态的贡献永远是$dp[j] + sum[j]$,只要对每个状态求这个值再取最大就可以直接$O(1)$获取之前答案的最大贡献了。问题是,前面乘了一个$(n - i)$后,之前状态的贡献就和当前的$i$有关系了。之前所有状态的部分贡献$dp[j],sum[j]$已经是完全确定的东西了,加上当前的$i$就能算出之前状态的全部贡献。那么,有没有一个仅通过当前的$i$来衡量之前哪个方案更优的方法呢?

如果之前存在两个状态$a,b$,不妨设$a < b$。那么,当从$a$比从$b$转移更优的时候是怎么样的呢?我们不妨列出此时两个状态的贡献值不等式,并且把含$i$的部分移到不等号的一边去。

由于前缀和数组递增,$sum[a] > sum[b]$,不等式不用变号。

也就是说,通过计算两个状态的$dp$数组差除以$sum$数组差,并将这个结果与$n - i$比较,我们就能知道之前两个状态孰优孰劣。这个计算出的值是反映状态好坏的本质属性,因为这个值的计算与当前的$i$无关。由于这个计算值和数学上斜率的计算十分相似,因此才有斜率优化这个名字。

如果把之前所有状态都看成一个$(sum[j], dp[j])$的二维点,现在我们知道了$n - i$的值,那么之前哪个状态最优呢?

我们从左到右从两两相邻的点连线。如果两两相邻的点的斜率是递减的,那就好办了:这就说明,前面有很多点对的斜率大于$n - i$,后面有很多点对的斜率小于$n - i$。那么中间的某个点就是最优的,那个点和它前一个点的斜率大于$n - i$,和它下一个点的斜率小于$n - i$。它比左边的点优,也比右边的点优。

问题是,如果出现了某三个从左到右相邻点$a, b, c$,且$ab$斜率小于$bc$呢?也就是相邻点对的斜率不是增减的。这种情况下,若斜率$ab$和$bc$都比$n - i$小,则最左边的$a$点最优;反之,若两个斜率都比$n - i$大,则最右边的点$c$最优。剩下的情况只有斜率$ab$小于$n - i$,斜率$bc$大于$n - i$这种情况。但是,此时$a$比$b$优,$c$也比$b$优。可以看到,无论什么情况下,$b$都不是最优的。因此,$b$点永远不会是上一个最优状态,可以把它“抛走”,再也不考虑它了。

也就是说,可能作为上一个最优状态的点,它们的$(sum[j], dp[j])$构成的二维点的斜率永远是单调递减的。这个单调性,是斜率优化DP的根本原因。

有了上述的分析,选上一个最优状态,不再需要枚举每一个之前的状态,而是维护一个可能作为上一状态的集合就可以了。这个集合从左到右存了一系列的可能的状态。每次要找最优状态的话,就像之前分析的那样,找到第一个斜率小于的$n - i$的左边的点,那个点对应状态就是最优的上一状态。由于单调性的存在,这个过程可以用二分完成。但由于我们每次的$n - i$是一个递减的量,也就是说我们每次需要找的斜率只会越来越小。那么那些斜率很大的点都可以扔掉去。因此,在本题中,可以不去二分查找,而去删掉前面那些已经斜率过大的点。最后剩下来的第一个点,它和下一个点的斜率小于$n - i$,它就是本次要寻找的最优上一状态。这个集合可以用双端队列Deque来维护。

同时,这个集合还需要进行维护。每次新得到了一个dp值后,我们就要试图把新的状态加入这个集合中。为了保证集合中点斜率的单调性,我们要取出集合最后两个点,拿它们和当前的点算出两个斜率,看看这个斜率是否满足单调递减的性质。如果不满足,就把中间的那个点去掉,也就是把原来集合中的最后一个点去掉。

Deque实现斜率优化DP方法简述

其实上面那道题并不适合用来学习斜率DP,因为它实际上牵扯的细节很多,dp式子并没有上面写得那么简单。HDU3507应该是最适合写的第一道斜率优化DP题。

这道题状态转移的式子比上面那个例子要复杂一点,而且是取最小值,需要维护的斜率应该满足单调递增而不是单调递减,但本质还是对前缀和什么的做一些运算,再进行斜率的比较,最终优化的方法还是完全一样的。

这道题中,不等式右边的与当前状态$i$有关的值是$sum[i]$,它是一个单调递增的量,正好与我们维护的是一个单调递增的斜率集合“相符合”(这里“符合”的意义比较微妙,也就是说,如果这里$sum[i]$不是单调递减,是递增或是无规律的,那么就不能用Deque来做了)。因此,我们在找最优状态的时候,不用去二分的找第一个斜率大于某个值的点,而是把前面斜率小的点都删掉去,把剩下来的第一个点作为上一状态。这个东西可以用Deque维护。

因此,在写代码时,应该按照下列步骤。

  1. 读入数据,初始化dp等数组,该求前缀和求前缀和。
  2. 声明一个deque\< int >,作为上一状态的集合。如果0位置的状态一开始就能被转移,就往deque里push一个0。
  3. 开始for循环,遍历每一个当前状态$i$。
  4. for循环体中:如果deque的大小大于等于2,说明deque中可能存在斜率不合法的点,循环寻找是否存在这样的点。每次循环得到front(),再pop_front(),再得到front(),也就是取出队列中前面两个点。判断这这两点的斜率,若斜率小于(大于)关键值(本例是2 * sum[i]),则continue掉,也就是扔掉第一个点,继续循环判断;否则,把取出的第一个点push_front()放回去,退出循环。这样,第一个斜率大于(小于)关键值的点就变成了que.front()。
  5. 不妨设j = que.front(),这个j就是最优的上一状态。用推好的dp式子计算dp[i]。
  6. 把状态i加入队列并维护队列的单调性。和开始的操作类似,循环判断deque大小是否大于等于2,若是,则取出队列队尾的两个点,计算它们和新的点$i$的两个斜率是否满足应该具有的单调性。若不满足单调性,则扔掉队尾的点,循环继续。
  7. 把状态i用push_back()扔进队列的队尾。
  8. for循环结束
  9. 输出dp数组中的答案。

二分查找实现斜率优化DP方法简述

CodeForces 674C也是一道斜率优化DP的题目。这道题递推式子相较之前两题更复杂了一些,而且它进行比较的和$i$有关的关键值并不是单调的。因此,这题不能再用Deque来维护最优上一答案,而是要用一个vector来存可能的状态,并进行二分查找。

写代码的步骤也类似,只是寻找上一最优答案的过程有所变更。

  1. 读入数据,初始化dp等数组,求好所需变量。
  2. 声明一些vector\< int >,作为上一状态的集合。如果dp某一维的0位置的状态一开始就能被转移,就往对应vector里push一个0。
  3. 开始for循环,遍历每一个当前状态$i$。如果dp数组和本题一样是多维的,那么再加一层for循环,按照顺序遍历dp数组的每一维。
  4. for循环体中:如果对应vector数组是空的,那么说明该状态不存在上一状态,直接跳过;否则,声明一个存储最优上一个状态的变量ii。ii的初始值是vector数组的第一项,因为我们将要二分查找的是最后一个斜率满足不等式的右端点,若二分查找什么都没找到,那么说明最优的点就是数组中的第一项。
  5. 二分查找最后一个斜率满足不等式的右端点。实现时把二分查找的mid对应状态套入不等式判断即可,若满足不等式则更新临时变量ii为vector中mid处对应的状态。
  6. 二分查找结束,ii就是最优的上一状态。用推好的dp式子计算dp[i]。
  7. 把状态i加入数组并维护数组的单调性。循环判断deque大小是否大于等于2,若是,则取出数组尾的两个点,计算它们和新的点$i$的两个斜率是否满足应该具有的单调性。若不满足单调性,则扔掉数组尾的点,循环继续。
  8. 把状态i用push_back()扔进数组尾。
  9. for循环结束
  10. 输出dp数组中的答案。

写代码的注意事项

写代码的时候会发现,不等式左边分子的那个量会被反复地用到。用一个函数来计算这个值会更加方便可靠。

在比较相邻两个斜率,而分子分母都是整数的时候,得用交叉相乘来比较斜率,规避除法。

总结/感想

算法原理

我算是大概理解了斜率优化DP的原理,但对什么时候可以用这个算法还有一点模糊。斜率优化DP本质上是在寻找最优上一状态时,找到衡量每一个状态的本质量,也就是那个二维点。对于每一对二维点,都有同一个判断它们两个谁更优的标准,那就是比较二者斜率是大于还是小于某个值(等于的情况可以忽略)。由于判断条件相同,只有斜率单调的点才可能成为最优上一状态。又由于有了单调的性质,才能在状态中进行二分查找。如果进行判断的斜率值还相对应是单调的,那么二分查找都可以省略,直接就可以得到最优的上一个状态。

能用斜率DP优化问题的核心原因就在于,对每一对二维点,判断它们谁更优的标准是一样的。但是,如果对于一个dp递推式子推导出的不等式中,作为分母的不是$(sum[a] - sum[b])$,而是另一组值,这一组值并不满足单调。这样的话,哪怕对于$a > b$的点,算出来的分母可能是个负数,除过去的话不等式要变号。对于相邻的两个点,有的时候斜率大于某个值更优,有的时候是斜率小于某个值更优。好像在这种情况下就不能用斜率优化DP了。

算法适用情况

能够用斜率优化DP的题都有类似的地方。这些题的dp在计算新值时,是在旧dp值的基础上加上一个和当前状态$i$有关的贡献。这个贡献往往是和上一位置到这一位置的区间和有关,以保证不等式分母是永远是正的。当看到一个类似这样的DP,且数据范围不允许$O(n^2)$时,就要考虑用斜率优化DP了。

和大部分DP题目一样,这种题目推式子是最重要的,代码实现上只要写个2、3题就能学会套路,很难出错。