你没听过的新型DP优化

· · 个人记录

众所周知,我们的笔者是一个非常懒的人(懒还写博客)

十分恐惧代码量大的题目(被一道两个月还没调出来的treap卡出心理阴影)

然后现在又被单调队列卡了(人懒得打)

但是面对那些需要单调队列优化的DP题目,我们难道就束手就擒了吗?

不是的

对于传统的单调队列优化DP,我们除了顺着题意被迫使用单调队列以外,我们还有一种船新的优化方式。(而且代码量巨小)

抛弃传统DP的思路

我们传统的DP一般都是通过一个dp[i]dp[i-1]之间的递推式来进行DP。

也就是说我们每次是拿过去的状态来更新当前的状态

在这里,我们要换一个思路。

我们既然可以用过去的状态去更新现在的状态,那么我们是不是也可以用现在的状态来更新将来的状态。(这个实现过程有点像素数筛)

也就是说我们的递推式是关于dp[i]dp[i+1]的。

个人认为比较模板的题目

(如果对于单调队列优化DP没有什么印象的同学可以看一下这个题面)

单调队列优化的题目一般传统的递归方式是这样的:

dp[i]=max(dp[i-x])+a_i 1<=x<=$某个定值$j

也就是我们拿过去的某些状态的最大值来更新当前状态。

而我们把它转化成dp[i]dp[i+1]的形式就成了:

dp[i+x]=max(dp[i+x] , dp[i]+a_g) 1<=x<=j 因此我们对于每一个$dp[i]$我们拿他去更新$dp[i+1]$~$dp[i+j]$。 那么对应带代码上就是这样(P1725的代码) ```cpp #include<cstdio> #define SIZE 400000 #define MIN -999999999 using namespace std ; int n , l , r ; int dp[SIZE] ; int a[SIZE] ; inline int max ( int a , int b ) { if ( a > b ) { return a ; } return b ; } int main ( ) { scanf ( "%d %d %d" , &n , &l , &r ) ; for ( int i = 0 ; i <= n ; i ++ ) { scanf ( "%d" , &a[i] ) ; } for ( int i = 0 ; i < SIZE ; i ++ ) { dp[i] = MIN ; } dp[0] = 0 ; for ( int i = 0 ; i < n ; i ++ ) { //我们是去枚举未来,所以是从0到n-1 for ( int j = 1 ; j <= r ; j ++ ) { dp[ i + j ] = max ( dp[ i + j ] , dp[i] + a[ i + j ] ) ; //和正常DP的差别在这里 } } int ans = MIN ; for ( int i = 0 ; i <= n ; i ++ ) { ans = max ( ans , dp[i] ) ; } printf ( "%d\n" , ans ) ; return 0 ; } ``` 到这里肯定有人要说了。 你这个写法根本没有优化啊,不就是换一个$O(N^2)$的朴素DP吗。 的确,写到这里本质上并没有对朴素DP又优化的作用 **但不代表后面没有啊** ## “类筛”优化 (正文开始~~之前都是水长度的~~) 我们先来看一下改写后的DP的实现形式: ![实现1](https://cdn.luogu.com.cn/upload/image_hosting/1tgehsty.png) 我们先拿$dp[2]$更新$dp[5] ,dp[6],dp[7]

然后我们再拿dp[3]去更新dp[6],dp[7],dp[8]

这时我们发现了一个神奇的事情(敲黑板)

因为我们的dp[7]已经被更大的dp[2]更新过了,因此我们dp[3]的更新肯定是无效的

同样我们的dp[6]也是不需要dp[3]来更新的。

因此,dp[3]dp[2]对比,它有效的地方仅仅在于它能够更新到dp[8]

因此我们可以总结出来一个性质:

我们就可以利用这个性质来对我们的DP进行优化

对于每一个dp[i]能够更新到的dp[i+l]~dp[i+r],我们倒着枚举l~r,如果出现dp[i+x]>dp[i]+a[i+x],即更新失败的情况。我们可以直接跳出该层的循环。

对应到代码中就是这样的:

for ( int i = 0 ; i < n ; i ++ ) { //顺着枚举dp[i]
        for ( int j = r ; j >= l ; j -- ) { //倒着枚举每个能够更新到的dp[i+j]
            if ( dp[ i + j ] > ( dp[i] + a[ i + j ] ) ) {
                break ; //如果更新失败,则直接退出
            }
            dp[ i + j ] = dp[i] + a[ i + j ] ;
        }
    }

优化复杂度:O(玄学)

和单调队列优化比较的特点

(注意这里是特点不是优点)

这里补充一下,被卡的意思是如果碰到的数据是严格升序的话,这个算法就会退化成O(N^2)

当然题目里面的数据一般不会卡这种花里胡哨的优化的。

应用范围

一般单调队列优化的DP都可以用这种玄学优化来做

可以理解为DijkstraSPFA的关系吧。(为SPFA沉痛默哀一秒)

同样花里胡哨的命名

我在前文也有意无意地暗示说了,因为觉得他的实现过程长得有点像素数筛,所以我个人称之为类筛优化。(当然这种小算法是不配用名字命名的)

(全文完结,撒花

大家都很强,可与之共勉