SDSC整理(Day1 单调队列和单调队列优化dp)
单调队列以及单调队列优化dp
你会发现我终于不写 \(Day2\) 的图论了
感觉这篇有点敷衍了事了,但是没办法,我太弱了
单调队列
顾名思义,单调队列就是一个元素满足单调性的队列。。。
然后呢?没有然后了
我们来看一道例题了解一下
单调队列的典型应用之一就是求一段区间内的最大值和最小值。
我们以样例求最大值为例,来模拟一下单调队列的维护过程
有序列 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
我们来看这样一道题
一道比较明显的 \(dp\) 题目。
我们假设 \(dp[i]\) 表示考虑到第 \(i\) 个格子所能得到的最大冰冻指数,
那么状态转移方程就是
朴素算法就是枚举这个 \(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\) 的好成绩。
其意义在于展示了不动脑子能拿的分数以及洛谷数据的强弱程度
下面是重点,我们考虑优化
首先观察我们的状态转移方程:
我们发现,在枚举 \(j\) 的时候对于 \(a[i]\) 是没有影响的,
所以我们可以把 \(a[i]\) 提出来,就变成了:
\(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\) 的步骤:
-
推出状态转移方程(dp都不会何来优化)
-
提出与枚举变量无关的常量
-
找出维护量,用单调队列维护
例题: