SDSC整理(Day1 单调队列和单调队列优化dp)

单调队列以及单调队列优化dp

你会发现我终于不写 \(Day2\) 的图论了

感觉这篇有点敷衍了事了,但是没办法,我太弱了

单调队列

顾名思义,单调队列就是一个元素满足单调性的队列。。。

然后呢?没有然后了

我们来看一道例题了解一下

P1886 滑动窗口 /【模板】单调队列

单调队列的典型应用之一就是求一段区间内的最大值和最小值。

我们以样例求最大值为例,来模拟一下单调队列的维护过程

有序列 1 3 -1 -3 5 3 6 7

\(i=1\)\(1\) 入队,此时的队列 \(\{ 1 \}\)

\(i=2\)\(3>1\) ,不满足单调性,\(1\) 出队, \(3\) 入队,此时的队列 \(\{ 3 \}\)

\(i=3\)\(-1<3\) ,满足单调性, \(-1\) 入队,此时的队列 \(\{ 3,-1 \}\)

\(i \ge k\) ,输出队头 \(3\)

\(i=4\)\(-3<3\) ,满足单调性, \(-3\) 入队,此时的队列 \(\{ 3,-1,-3 \}\)

\(i \ge k\) ,输出队头 \(3\)

\(i=5\)\(q[head] \le i-k\)\(3\) 出队, \(5>-1\) ,不满足单调性,\(-1\) 出队, \(5>-3\)\(-3\) 出队, \(5\) 入队,此时的队列 \(\{ 5 \}\)

\(i \ge k\) ,输出队头 \(5\)

\(i=6\)\(3<5\) ,满足单调性, \(3\) 入队,此时的队列 \(\{ 5,3 \}\)

\(i \ge k\) ,输出队头 \(5\)

\(i=7\)\(6>5\) ,不满足单调性, \(5\) 出队, \(6>3\)\(3\) 出队, \(6\) 入队,此时的队列 \(\{ 6 \}\)

\(i \ge k\) ,输出队头 \(6\)

\(i=8\)\(7>6\) ,不满足单调性, \(6\) 出队, \(7\) 入队,此时的队列 \(\{ 7 \}\)

\(i \ge k\) ,输出队头 \(7\)

累死我了

code

/*
	单调队列
   	date:2022.7.28
   	worked by respect_lowsmile
*/
#include<iostream>
#include<queue>
using namespace std;
const int N=1e6+5;
int q[N],a[N];
int n,k,head=1,tail=0;
int main()
{
	scanf("%d %d",&n,&k);
	for(int i=1;i<=n;++i)
	  {
	  	scanf("%d",&a[i]);
	  	while(head<=tail&&q[head]<=i-k) head++;
	  	while(head<=tail&&a[q[tail]]>=a[i]) tail--;
	  	tail++;
	  	q[tail]=i;
	  	if(i>=k) printf("%d ",a[q[head]]);
	  }
	printf("\n");
	head=1,tail=0;
	for(int i=1;i<=n;++i)
	  {
	  	while(head<=tail&&q[head]<=i-k) head++;
	  	while(head<=tail&&a[q[tail]]<=a[i]) tail--;
	  	tail++;
	  	q[tail]=i;
	  	if(i>=k) printf("%d ",a[q[head]]);
	  }
	return 0;
} 

STL有个好东西,叫做双端队列,它支持队头和队尾的删除和添加操作。

头文件 #include<deque>

deque<int> q;   创建一个 int 类型的双端队列

q.push_back()   在队列末尾添加一个元素

q.push_front()   在队列头部增加一个元素

q.pop_front()   删除队头的第一个元素 

q.pop_back()    删除队尾的第一个元素

q.empty()      队列为空就返回1,否则返回0

q.size()      返回队列中元素的个数

q.erase()      删除队列指定位置的元素,可以是一个位置,也可以是一个区间

q.clear()     清空队列

单调队列优化dp

我们来看这样一道题

P1725 琪露诺

一道比较明显的 \(dp\) 题目。

我们假设 \(dp[i]\) 表示考虑到第 \(i\) 个格子所能得到的最大冰冻指数,

那么状态转移方程就是

\[dp[i]=max(dp[j]+a[i])\ (i-R \le j \le i-L) \]

朴素算法就是枚举这个 \(j\) ,然后求最大值。

for(int i=0;i<=n;++i)
	for(int j=i-R;j<=i-L;++j)
		if(j>=0)  dp[i]=max(dp[i],dp[j]+a[i]);

那我们的结果就是 \(n-R+1\)\(n\) 这个范围内的最小值。

我们知道 \(dp\) 的复杂度等于状态 \(\times\) 转移,所以朴素算法的时间复杂度是 \(O(n^2)\) ,光荣的获得了 \(60pts\)好成绩

其意义在于展示了不动脑子能拿的分数以及洛谷数据的强弱程度

下面是重点,我们考虑优化

首先观察我们的状态转移方程:

\[dp[i]=max(dp[j]+a[i])\ (i-R \le j \le i-L) \]

我们发现,在枚举 \(j\) 的时候对于 \(a[i]\) 是没有影响的,

所以我们可以把 \(a[i]\) 提出来,就变成了:

\[dp[i]=max(dp[j])+a[i]\ (i-R \le j \le i-L) \]

\(max(dp[j])\ (i-R \le j \le i-L)\) 你能想到什么,这不就是区间长度固定的查询区间最值吗?!!!

我们用单调队列维护 \([i-R,i-L]\)\(dp\) 最大值,然后从小到大枚举 \(i\) 即可,时间复杂度 \(O(n)\)

#include<iostream>
using namespace std;
const int N=2e5+5;
const int INF=0x3f3f3f3f;
int dp[N],ice[N],q[N];
int n,L,R,ans=-INF;
int main()
{
	scanf("%d %d %d",&n,&L,&R);
	for(int i=0;i<=n;++i)
		scanf("%d",&ice[i]);
	for(int i=1;i<=n;++i)
		dp[i]=-INF;
	dp[0]=0;
	int head=1,tail=0;
	for(int i=L;i<=n;++i)
	{
		while(head<=tail&&dp[q[tail]]<=dp[i-L])  tail--;
		tail++;
		q[tail]=i-L;
		while(head<=tail&&q[head]<i-R) head++;
		dp[i]=dp[q[head]]+ice[i];
		if(i+R>n)  ans=max(ans,dp[i]);
	}
	printf("%d",ans);
	return 0;
}

solution

单调队列优化动态规划问题的基本形态:

当前状态的所有值可以从上一个状态的某个连续的段的值得到,

要对这个连续的段进行 RMQ 操作,

相邻状态的段的左右区间满足非降的关系。

\(End\)

单调队列优化 \(dp\) 的步骤:

  1. 推出状态转移方程(dp都不会何来优化)

  2. 提出与枚举变量无关的常量

  3. 找出维护量,用单调队列维护

例题:

P3957 [NOIP2017 普及组] 跳房子

P2034 选择数字

P2627 [USACO11OPEN]Mowing the Lawn

P3572 [POI2014]PTA-Little Bird

posted @ 2022-11-12 10:27  respect_lowsmile  阅读(15)  评论(0编辑  收藏  举报