单调队列
定义
是指队列维护的元素单调、下标也单调的数据结构。
单调队列不像优先队列,是一种C++自带的STL,单调队列是用普通的队列进行维护的。
使用场景
滑动窗口。
在一个固定大小的窗口内寻找最值,且窗口从左到右移动。
滑动窗口实现方法
-
暴力: O ( n 2 ) O(n^2) O(n2)
-
线段树: O ( n × log 2 n ) O(n \times \log_2{n}) O(n×log2n)。
-
RMQ: O ( n × log 2 n ) O(n \times \log_2{n}) O(n×log2n)
-
单调队列: O ( n ) O(n) O(n)
显然,用单调队列维护时间复杂度最优。
单调队列实现步骤
-
循环枚举下标 i i i,从 1 1 1 到 n n n。
-
a i a_i ai 循环与队尾元素 a q t a i l a_{q_{tail}} aqtail 比较,并删除 ≤ a i \le a_i ≤ai 的队尾, i i i 进队尾。
-
检查队头是否过期,并从队头删除过期下标。
-
输出队头元素,即为当前窗口最大值(具体看题目)。
时间复杂度分析:每个元素只出队入队一次,总时间复杂度为 O ( n ) O(n) O(n)
例题
First:P1886、P2032
单调队列模板题。
放个P1886的代码
cpp
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
int a[N], q[N], n, k, head=1, tail;
void getmin()
{
head=1, tail=0;
for(int i=1;i<=n;i++)
{
while(head<=tail&&a[q[tail]]>=a[i])
{
tail--;
}
q[++tail]=i;
while(head<=tail&&q[tail]-q[head]+1>k)
{
head++;
}
if(i>=k)cout<<a[q[head]]<<" ";
}
cout<<"\n";
}
void getmax()
{
memset(q,0,sizeof(q));
head=1, tail=0;
for(int i=1;i<=n;i++)
{
while(head<=tail&&a[q[tail]]<=a[i])
{
tail--;
}
q[++tail]=i;
while(head<=tail&&q[tail]-q[head]+1>k)
{
head++;
}
if(i>=k)cout<<a[q[head]]<<" ";
}
cout<<"\n";
}
signed main()
{
cin>>n>>k;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
getmin();
getmax();
}
Second:P1638、P1440
稍微包装了一下的模板题。
Third:P1714
单调队列中套了一个简单算法,这次套的前缀和。
Fourth:P2698、P2564
难度上升了不止一点。
P2698在二次单调队列中套了个二分。
难度都差不多,P2564有一些坑点,P2698难写一些。
P2698也有坑点,和单调队列窗口大小有关。
总结
单调队列虽然是线段树等数据结构中难度中下级的数据结构,但与其他算法结合起来难度还是不小的。
单调队列例题
讲个难点的,P2698
分析
-
由于每滴水以 1 1 1 单位时间下降,所以时间差等于高度差。
-
假设花盆宽度 w w w 已经确定,那么花盆可以从左到右滑动,转化为滑动窗口。
-
维护 2 2 2 个单调队列,维护最小和最大 y y y 值。
-
枚举花盆宽度可行但会TLE,宽度有单调性,一眼二分。
注意事项:
-
枚举将水滴按 x x x 轴 s o r t sort sort。(水滴比坐标小,枚举水滴常数低)
-
花盆宽度为 n n n 时,可接到 [ i , i + w ] [i,i+w] [i,i+w] 的水滴。
贴个 check 函数
cpp
bool check(int x)
{
head=1,tail=0,head1=1,tail1=0;
for(int i=1;i<=n;i++)
{
while(head<=tail&&a[i].y>=a[q[tail]].y)
{
tail--;
}
q[++tail]=i;
while(tail-head>=0&&a[q[tail]].x-a[q[head]].x+1>x)
{
head++;
}
while(head1<=tail1&&a[i].y<=a[qq[tail1]].y)
{
tail1--;
}
qq[++tail1]=i;
while(tail1-head1>=0&&a[qq[tail1]].x-a[qq[head1]].x+1>x)
{
head1++;
}
if(abs(a[q[head]].y-a[qq[head1]].y)>=m)
{
return 1;
}
}
return 0;
}
单调栈
定义
一种下标单调、元素也单调的栈。
单调栈同单调队列不是一种C++自带的STL,单调栈是用普通的栈进行维护的。
使用场景
在若干区间内找最值,转化为枚举每个最值找区间。
寻找每个元素 a i a_i ai 向右(左)第一个比 a i a_i ai 大(小)的元素位置。
如何寻找 a i a_i ai 右边第一个大于 a i a_i ai 的位置?
-
枚举 i i i, a i a_i ai 与 s t k . t o p ( ) stk.top() stk.top() 循环比较,若 a i a_i ai 大于当前栈顶元素,弹出栈顶。
-
i i i 入栈。
-
循环结束后,剩余栈中元素下标说明其右侧没有更大的元素。
如何寻找 a i a_i ai 左边第一个大于 a i a_i ai 的位置?
同上,倒序枚举 i i i 即可。
训练题单
-
P5788 模板题
-
P2947 跟模板题差不多,套了个背景。
-
P2866 初学者建议去做,码量短,有少许思维难度。
-
CF547B 做法多样,难度中等偏上少许,有一定思维难度,记得看讨论区的翻译。
-
CF1299C 这题建议已开始学CSP-S相关内容的同学做,难度思维码量都有的。
模板代码
cpp
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e6+5;
int n, a[N], ans[N];
stack<int> stk;
signed main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
a[n+1]=1e9;
}
for(int i=1;i<=n;i++)
{
while(!stk.empty()&&a[i]>a[stk.top()])
{
ans[stk.top()]=i;
stk.pop();
}
stk.push(i);
}
for(int i=1;i<=n;i++)
{
cout<<ans[i]<<" ";
}
return 0;
}