单调队列优化DP
1.单调队列
1.1单调队列的性质
单调队列是一种特殊的双端队列,其内部元素就有单调性
设有一个长为
则单调队列中的元素一定是按照队列的方式,先进先出的,按照单调的方式,优先级递增或递减的(下面用递减)
则对于当前位置
在动态规划中,单调队列要保证队列中每一个元素的下标和(动规的)状态值满足单调性(递减或递增)
由于每个元素最多出队和进队各一次,所以负责度是
可以在一定情况下,维护子区间的最值
1.2单调队列的应用
1.2.1 定长连续子序列问题
【例.1】P1886 滑动窗口 /【模板】单调队列
题目描述:
有一个长为
样例输入:
8 3 1 3 -1 -3 5 3 6 7
样例输出:
-1 -3 -3 -3 3 3 3 3 5 5 6 7
思路分析:
| Window position | Minnimum value | Maxnimum value |
|---|---|---|
| [1 3 -1] -3 5 3 6 7 | -1 | 3 |
| 1 [3 -1 -3] 5 3 6 7 | -3 | 3 |
| 1 3 [-1 -3 5] 3 6 7 | -3 | 5 |
| 1 3 -1 [-3 5 3] 6 7 | -3 | 5 |
| 1 3 -1 -3 [5 3 6] 7 | 3 | 6 |
| 1 3 -1 -3 5 [3 6 7] | 3 | 7 |
以下分析以最小值为例,最大值同理
不难看出,对于第
维护一个单调队列
当前为第
每次计算答案前需要判断队首的元素在原序列中的位置,来判断是否在结尾为
将自己入队,统计答案即可
代码实现:
数组模拟队列的实现代码:
#include<cstdio>
const int maxn(1e6+5);
int n,k,h=1,t,q[maxn],a[maxn];//h表示队头,t表示队尾,q维护单调队列
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
{
scanf("%d",a+i);
while(h<=t&&a[q[t]]>=a[i]) t--;
q[++t]=i;//将自己入队
if(q[h]+k<=i) h++;//判断并踢出过期的队头
if(i>=k) printf("%d ",a[q[h]]);
}//求最小值
printf("\n");
t=0,h=1;
for(int i=1;i<=n;i++)
{
while(h<=t&&a[q[t]]<=a[i]) t--;
q[++t]=i;
if(q[h]+k<=i) h++;
if(i>=k) printf("%d ",a[q[h]]);
}//求最大值
return 0;
}
介绍一个STL:#include<deque>
q.pop_back()
q.pop_front()
q.push_back()
q.push_front()
q.front()
q.back()
q.empty()
用STL实现的单调队列
以【例.1】代码为例
#include<cstdio>
#include<deque>
const int maxn(1e6+5);
std::deque <int> q;
int n,k,a[maxn];
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
{
scanf("%d",a+i);
while(!q.empty()&&a[q.back()]>=a[i]) q.pop_back();
q.push_back(i);
if(!q.empty()&&q.front()+k<=i) q.pop_front();
if(i>=k) printf("%d ",a[q.front()]);
}
while(!q.empty()) q.pop_back();
printf("\n");
for(int i=1;i<=n;i++)
{
while(!q.empty()&&a[q.back()]<=a[i]) q.pop_back();
q.push_back(i);
if(!q.empty()&&q.front()+k<=i) q.pop_front();
if(i>=k) printf("%d ",a[q.front()]);
}
return 0;
}
【例.2】#10176. 「一本通 5.5 例 2」最大连续和
题目描述:
给你一个长度为 的整数序列
思路分析:
进行
显而易见
由于你优秀的时间复杂度,所以你需要去优化它
发现可以用前缀和优化,设
则
看出来了嘛?单调队列?
在化简一下
对于
对于第
用单调队列维护即可
代码实现:
#include<cstdio>
#include<deque>
const int maxn(2e5+5);
int n,m,sum[maxn],x,ans=-2147483647;
std::deque <int> q;
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&x);
sum[i]=sum[i-1]+x;
}
for(int i=0;i<=n;i++)
{
if(!q.empty()&&q.front()<i-m) q.pop_front();
if(!q.empty()) ans=std::max(ans,sum[i]-sum[q.front()]);
while(!q.empty()&&sum[q.back()]>=sum[i]) q.pop_back();
q.push_back(i);
}
printf("%d\n",ans);
return 0;
}
1.2.2 单调队列在DP中的常见应用
【例.3】P2627 [USACO11OPEN]Mowing the Lawn G+P2034 选择数字
双倍经验!!
题目描述:
给定一行
思路分析:
先分析朴素DP:
设
则有
化简一下得到
然后就是单调队列的板子了,不同的是,这里维护的权值是
代码实现:
#include<cstdio>
#include<deque>
const int maxn(2e6+5);
int n,k;
long long sum[maxn],x,ans,f[maxn][2];
std::deque <int> q;
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
{
scanf("%lld",&x);
sum[i]=sum[i-1]+x;
}
for(int i=0;i<=n;i++)
{
if(i) f[i][0]=std::max(f[i-1][0],f[i-1][1]);
if(!q.empty()&&q.front()<i-k) q.pop_front();
if(!q.empty()) f[i][1]=sum[i]-sum[q.front()]+f[q.front()][0];
while(!q.empty()&&f[i][0]-sum[i]>f[q.back()][0]-sum[q.back()]) q.pop_back();
q.push_back(i);
}
printf("%lld\n",std::max(f[n][0],f[n][1]));
return 0;
}
后附描述:
【习.1】#10180. 「一本通 5.5 练习 1」烽火传递
【习.2】#10181. 「一本通 5.5 练习 2」绿色通道
1.2.3 单调队列优化多重背包
【例.4】P3423
题目描述:
(BBB) 拥有一套先进的货币系统,这个系统一共有
做法:
1.多重背包 TLE
2.二进制优化 AC
3.单调队列优化 AC
1.3 推荐习题
【习.3】P1440
【习.4】P2216
【习.5】P2569