单调队列优化 DP

单调队列优化dp


“单调队列非常适合优化 决策取值范围的上、下界随枚举阶段单调变化,同时每个决策在候选集合中插入或删除至多一次 的 dp 最优化问题 ”

核心思想在于将 i − 1 i-1 i1 的决策集合以 较小的时间代价 转移至 i i i 上,维护决策集合的 高效性有序性

高效 指候选集合中尽可能 排除不优的决策

有序 指在当前阶段进行决策时,能够快速得到 最优解(通常与数据结构结合)

例1 —— 精确决策上下界

在这里插入图片描述
n ≤ 16000 n \leq16000 n16000
K ≤ 100 K \leq100 K100

单调队列优化 DP 的常见套路是:从朴素思路入手,写出 复杂度较高 而 直观 的状态转移方程,标清楚 决策 的上下界,然后依据 转移 的性质用 单调队列维护 决策集合 ,进而优化复杂度

对本题,设 f [ i ] [ j ] f[i][j] f[i][j] 表示考虑前 i i i 个木匠,当前刷到了第 j j j 块板,最大报酬

首先有: f [ i ] [ j ] = f [ i − 1 ] [ j ] , f [ i ] [ j ] = f [ i ] [ j − 1 ] f[i][j]=f[i-1][j],f[i][j]=f[i][j-1] f[i][j]=f[i1][j]f[i][j]=f[i][j1] 这是第一步的转移,去除了有空板 / 空人的影响

那么接下来考虑:令第 i i i 个人刷第 j j j 块板,假设第 i − 1 i-1 i1 个人考虑到第 k k k 块,如何转移?

f [ i ] [ j ] = max ⁡ (   f [ i − 1 ] [ k ] + P [ i ] × ( j − k )   ) f[i][j]=\max(\ f[i-1][k]+P[i]\times (j-k)\ ) f[i][j]=max( f[i1][k]+P[i]×(jk) )

显然有限制: k < S i ≤ j k< S_i\leq j k<Sij j − k ≤ L i    ⟺    j − L i ≤ k < S i j-k\leq L_i \iff j-L_i\leq k<S_i jkLijLik<Si

观察这个内层枚举的 k k k 的上下界:随 j j j 的增大, k k k 的左端点增大 1 1 1,符合使用单调队列的第一个条件

观察 DP 式子,从 集中变量 的角度出发,我们将其写作:

f [ i ] [ j ] = P [ i ] × j + max ⁡ (   f [ i − 1 ] [ k ] − P [ i ] × k ) f[i][j]=P[i]\times j+\max(\ f[i-1][k]-P[i]\times k) f[i][j]=P[i]×j+max( f[i1][k]P[i]×k)

显然,后面的这个 m a x max max 函数与 j j j 无关,我们可以将其作为一个整体 g [ k ] g[k] g[k] 放到 单调队列中维护

下面是 单调队列优化 DP 中最重要的部分:排除无用决策

不妨设 决策 k 1 < k 2 k_1<k_2 k1<k2,显然,随着 j j j 的增大, k 1 k_1 k1 会被先排除出决策集合

g [ k 1 ] ≤ g [ k 2 ] g[k_1]\leq g[k_2] g[k1]g[k2],则 k 2 k_2 k2 一定 比 k 1 k_1 k1 更优,将 k 1 k_1 k1 出队

综上,我们需要维护的是一个 决策 k 单调递增,值 g [ k ] g[k] g[k] 单调递减 的队列

支持从队头删除元素,队尾插入并维护 g [ k ] g[k] g[k] 单调性

#include<bits/stdc++.h>
using namespace std ;

const int N = 16010 , M = 110 ;
typedef long long LL ;

int n , K ;
struct nn
{
  int l , v , pos ;
}a[N] ;
bool cmp( nn x , nn y )
{
  return x.pos < y.pos ;
}
int f[M][N] , ans ;
deque<int> q ;

int main()         
{
  scanf("%d%d" , &n , &K ) ;
  for(int i = 1 ; i <= K ; i ++ ) {
  	scanf("%d%d%d" , &a[i].l , &a[i].v , &a[i].pos ) ;
  }
  sort( a+1 , a+K+1 , cmp ) ;
  for(int i = 1 ; i <= K ; i ++ ) {
  	while( !q.empty() ) q.pop_back() ;
  	q.push_back( 0 ) ;
  	for(int j = 1 ; j <= n ; j ++ ) {
  		f[i][j] = max( f[i-1][j] , f[i][j-1] ) ;
  		while( !q.empty() && ( q.front() < j-a[i].l ) ) {
  			q.pop_front() ;
  		}
  		if( !q.empty() && j >= a[i].pos && j <= a[i].pos+a[i].l-1 ) {
  			f[i][j] = max( f[i][j] , a[i].v*j + f[i-1][q.front()]-a[i].v*q.front() ) ;
  		}
  		if( j < a[i].pos ) { // 维护决策集合中的元素合法 
  			while( !q.empty() && f[i-1][j]-a[i].v*j >= f[i-1][q.back()]-a[i].v*q.back() ) {
  				q.pop_back() ;
  			}
  			q.push_back( j ) ; 
  		}
  		ans = max( ans , f[i][j] ) ;
  	}
  }
  printf("%d" , ans ) ;
  return 0 ;
}

这道题主要告诉我们,写好单调队列优化 DP 的一个必要前提是 标清楚决策的取值范围

集中变量的思想也很重要

例2 —— 单调队列维护决策,其他数据结构维护转移值

在这里插入图片描述

如果我们预处理出来 以每个数为结尾,能够向左延申的最大长度 L [ i ] L[i] L[i],显然有:

f [ i ] = min ⁡ (   f [ j ] + m a x [ a j + 1 , . . . , a i ]   ) f[i]=\min(\ f[j]+max[a_{j+1},...,a_i]\ ) f[i]=min( f[j]+max[aj+1,...,ai] ) ,其中 L [ i ] − 1 ≤ j < i L[i]-1\leq j<i L[i]1j<i

观察这个式子我们发现 与上一题不同,这里 m i n min min 函数中的整体同时与 i , j i,j i,j 两维有关,无法直接维护

那么从原点出发:维护 决策集合的 高效性有序性

首先考虑高效:

从性质上看,显然随着 j j j 的枚举, f [ j ] f[j] f[j] 单调递增,而 m a x ( j + 1 , i ) max(j+1,i) max(j+1,i) 单调不增

那么 当 max 值相等时,靠前的 决策 会更优

在这里插入图片描述
对比 j 1 , j 2 j_1,j_2 j1,j2 ,有共同的 m a x max max 值为 m a x 2 max2 max2

那么由于 f f f 的单增, 当二者在同一决策集合中 时 , j 1 j_1 j1 是更优的

更进一步,我们发现,真正优秀的决策必定是 每个后缀 max 的位置!!!

这说明我们只需维护一个 处理后缀 m a x max max 的数据结构,实时添加、删除

—— 那么这不就是单调队列的基本功能吗!!!

只有队列中的元素可能成为最优决策,保证决策集合的高效性

但是这样有一个问题:我们在考虑排除无用决策时,是用 前面的决策排除了后面的决策,而 决策集合 是从左向右滑动的,这样会不会导致某些后面的有效决策被排除?

我们再来思考这种情况怎么才会发生:某段后缀区间的 m a x max max位置的决策 排除了在它后面的部分元素,现在它出队了,谁有能是最优呢?

显然:决策集合的左端点!!!(由上面的性质发现,同一个 m a x max max 区间里越靠左越优)

所以转移除了单调队列中,还需要决策集合左端点

其次考虑有序:

单调队列中维护的是 a [ i ] a[i] a[i] 的有序,而非转移值即 min ⁡ (   f [ j ] + m a x [ a j + 1 , . . . , a i ]   ) \min(\ f[j]+max[a_{j+1},...,a_i]\ ) min( f[j]+max[aj+1,...,ai] ) 的有序

遇到这种情况时,我们要考虑再搞一个数据结构,维护最优的转移值

这个数据结构 与 单调队列 构成 一一映射关系

本题 线段树 / set 皆可,下面以 set 为例

但还是有问题在与: 后面那个 m a x max max 值 是会变的!!我们还需要实时修改

考虑将当前 a [ i ] a[i] a[i] 加入决策集合,思考:哪些转移值需要变?

这时维护高效性的功效就显现出来了:

加入 i i i 时,首先让部分决策出队,并从 set 中删除,不需要更改

准备让 i i i 入队时,只需要更改此时队头即可,因为 a i a_i ai 作为 Max 当且仅当在此时队头的决策中

修改降为 l o g   n log\ n log n,那么整体复杂度达到了 O ( n   l o g   n ) O(n\ log\ n) O(n log n)

细节还是非常多的

#include<bits/stdc++.h>
using namespace std ;

const int N = 1e5 + 100 ;
typedef long long LL ;
LL read()
{
	LL x = 0 ; char c = getchar() ;
	while( c > '9' || c < '0' ) c = getchar() ;
	while( c >= '0' && c <= '9' ) x = (x<<1)+(x<<3)+(c^48) , c = getchar() ;
	return x ;
}

int n , L[N] , a[N] , st[20][N] , Lg[N] , Max[N] ; // 记录 决策 j 的 max(j+1,i) 
LL m , f[N] ; 
void pre_work()
{
	Lg[1] = 0 ;
	for(int i = 2 ; i <= n ; i ++ ) Lg[i] = Lg[i/2] + 1 ;
	for(int i = 1 ; i <= 18 ; i ++ ) {
		for(int j = 1 ; j <= n-(1<<i)+1 ; j ++ ) {
			st[i][j] = max( st[i-1][j] , st[i-1][j+(1<<(i-1))] ) ;
		}
	}
}
int query( int l , int r )
{
	if( !l ) l = 1 ;
	int k = Lg[r-l+1] ;
	return max( st[k][l] , st[k][r-(1<<k)+1] ) ; 
}
struct nn
{
	int id ;
	LL num ;
	friend bool operator < ( nn x , nn y ) {
		return x.num < y.num || ( x.num == y.num && x.id < y.id ) ;
	} 
};
set<nn> s ;
deque<int> q ; // 单调队列维护 后缀Max 值 

int main()         
{
	n = read() , m = read() ;
	LL sum = 0 ;
	int l = 0 ;
	for(int i = 1 ; i <= n ; i ++ ) {
		a[i] = read() ;
		st[0][i] = a[i] ;
		if( a[i] > m ) {
			printf("-1\n") ;
			return 0 ; 
		}
		sum += a[i] ;
		while( sum > m ) {   
			sum -= a[l] ;
			l ++ ;
		}
		L[i] = l ;
	}
	pre_work() ;
	q.push_back( 0 ) ;
	a[0] = 1e9 ;// a[0]在出决策集合之前,不应被弹出队列
	Max[0] = a[1] ;
	s.insert( (nn){ 0 , 0+Max[0] } ) ;
	for(int i = 1 ; i <= n ; i ++ ) {
		// 1.从边界直接转移过来 
		f[i] = f[L[i]-1] + query( L[i] , i ) ;
		// 2.从set中取最优值
		while( !q.empty() && q.front() < L[i]-1 ) {//维护决策集合左端点
			s.erase( (nn){ q.front() , f[q.front()]+Max[q.front()] } ) ; 
			q.pop_front() ;
		}
		// 先用 a[i] 去更新 决策集合 
		while( !q.empty() && a[i] >= a[q.back()] ) {
			s.erase( (nn){ q.back() , f[q.back()]+Max[q.back()] } ) ;
			q.pop_back() ;
		}
		if( !q.empty() ) { // 更新 队尾的区间 Max ,单调队列维护最优决策,使得Max只用改变一个 
			s.erase( (nn){ q.back() , f[q.back()]+Max[q.back()] } ) ;
			Max[q.back()] = a[i] ;
			s.insert( (nn){ q.back() , f[q.back()]+Max[q.back()] } ) ;
			f[i] = min( f[i] , f[(s.begin()->id)]+query(s.begin()->id+1,i) ) ;
		}
		q.push_back( i ) ;
		s.insert( (nn){ i , f[i] } ) ; // 先用 f[i] 表示,再被后面的 a[i] 更新 
	}
	cout << f[n] ;
  	return 0 ;
}

例3 —— 结合某些性质,贪心保留最优决策

在这里插入图片描述
首先

整体上看,转移时需要维护到两个信息:当前层的长度,上一层的长度

其中第一个可以通过 以 i 为结尾,枚举开头,在本次转移中确定 贡献

而第二个 表面上看 不得不增加 DP 的一维 去维护,但复杂度是不允许的

那么基于 减少无用决策出发:当本层的高度最大时,只保留 最小的 长度即可

这样只需要额外开一个 g 数组 来存储最小长度信息,同时以 f 为第一关键字更新

#include<bits/stdc++.h>
using namespace std ;

const int N = 1e5 + 20 ;
typedef long long LL ;
int read()
{
	int x = 0 ; char ch = getchar() ;
	while( !isdigit(ch) ) ch = getchar() ;
	while( isdigit(ch) ) x = (x<<1)+(x<<3)+(ch^48) , ch = getchar() ;
	return x ;
}


int n , a[N] ;
int f[N] , g[N] , sum[N] ; 
deque<int> q ;

int main()
{
	n = read() ;
	for(int i = 1 ; i <= n ; i ++ ) {
		a[n-i+1] = read() ; 
	}
	q.push_back( 0 ) ;
	int pos = 0 ;
	for(int i = 1 ; i <= n ; i ++ ) {
		sum[i] = sum[i-1] + a[i] ;
		while( q.size() > pos+1 && sum[q[pos+1]]+g[q[pos+1]] <= sum[i] ) { // sum_i 单增 
			pos ++ ;
		}
		f[i] = f[q[pos]] + 1 , g[i] = sum[i] - sum[q[pos]] ;
		while( !q.empty() && sum[q.back()]+g[q.back()] >= sum[i]+g[i] ) {
			q.pop_back() ;
		}
		q.push_back( i ) ; // 倒着做使得 f[j] 具有单调性,取符合条件的最大的即可 
	}
	printf("%d" , f[n] ) ;
  	return 0 ;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值