浅谈单调队列优化DP

这篇是搬运自己在洛谷写的题解 题目链接:\(luoguP1725\) 相信读完题就能看出这是一道简单的DP题

@TOC

状态转移

状态转移方程很容易想到。

即:

dp[ i ] = max{区间内的最大dp值}+该点的权值

然而这种算法的最坏复杂度为$O(n^2)$,而本题的数据是$1e5$级别的,这样的复杂度显然不正确,必须优化。


优化

哪一步可以优化呢?

可以发现这样转移有着冗杂的枚举区间内元素的过程。

我们又发现每次跳的区间都是一个固定长度,自然而然地想到了一个经典的问题:滑动窗口问题,即单调队列,可以让我们做到$O(n)$预处理,$O(1)$找到任意固定长度区间内的最值。

有了单调队列,我们的dp过程可以省去枚举区间内的每个元素的过程,总复杂度也由$O(n^2)$降到$O(n)$,完全没有压力。


如何实现

在代码中由注释进行讲解:

#include
using namespace std;
#define oo 0x3f3f3f3f 
#define ri register int
const int N = 200005;
int n,l,r,head = 1,tail = 0,cur = 0,ans = -oo,a[N],q[N],dp[N];
//head,tail分别是单调队列的首尾指针,cur是指向当前待入队列的元素,a存权值,q为单调队列(手打) 
inline int max( int a , int b ){return a > b ? a : b; }//手打max 
template
inline void read(T &res){
    static char ch;T flag = 1;
	while( ( ch = getchar() ) < '0' || ch > '9' ) if( ch == '-' ) flag = -1;
	res = ch - 48;
	while( ( ch = getchar() ) >= '0' && ch <= '9' ) res = res * 10 + ch - 48;
	res *= flag;
}//快读 
int main()
{
    read( n );read( l );read( r );
    for( ri i = 0 ; i <= n ; i++ ) read( a[ i ] );
    for( ri i = 1 ; i <= n ; i++ ) dp[ i ] = -oo;//初始化 
    for( ri i = 1 ; i <= n ; i++ ){
    	//由于一个点可到达的区间为[i+l,i+r],因此可以到达一个点的区间为[i-r,i-l],注意不要越界 
    	while( i - cur >= l ){//让能够入队的结点入队 
    		while( head <= tail && dp[ cur ] >= dp[ q[ tail ] ] ) tail--;
			//由于单调队列是单调递减的,所以我们要删除队尾那些dp值小于待入队结点dp值的结点 
			q[ ++tail ] = cur++;//入队 
    	}
    	while( head <= cur && i - q[ head ] > r ) head++;
		//如果队列的头已经不再界限内,则不可能再更新当前 
    	if( head <= tail )//注意,当队列不为空时才可以转移 
		  dp[ i ] = dp[ q[ head ] ] + a[ i ];
    }
    //因为题目说只要下一步的位置编号大于N就算到达对岸 
	//所以最后得出答案,为区间[n-r+1,n]中的dp最大值 
    for( ri i = n ; i >= n - r + 1 ; i-- )
	  ans = max( ans , dp[ i ] );
    printf( "%d\n" , ans );
	return 0;
}

如果嫌这道题太简单,可以尝试\(P3957\) 跳房子。

你可能感兴趣的:(浅谈单调队列优化DP)