声明:文章同步发表于我的洛谷博客。
不得不说,这玩意是真的烧脑。我至少学了三遍才学会。
我们先看一道题:
P1886 滑动窗口 /【模板】单调队列
作为一名 OIer,我们肯定是优先上暴力大法(暴力该怎么写不用我说了吧)。
我们先拿单调栈试试(数据:1 3 -1 -3 5 3 6 7,区间长度为 ,先找最大值):
入栈,栈变为
。
入栈,栈变为
。
入栈,栈变为
(这还是求最大值吗?)。
入栈,栈变为
。
入栈,栈变为
。
入栈,栈变为
(这是区间吗……)。
入栈,此时我们现在遇到了一个问题:怎么把
扔出去。
到了这里,你应该就能发现,栈由于本身容器的限制,无法方便地实现窗口最值查询(当然如果你能用单调栈做出来那必须 Orz)。
因此我们考虑使用队列进行维护,准确来说是双端队列。
STL 双端队列(deque)使用方法(局部):
成员函数 | 功能 |
deque<int>q | 定义一个 int 类型的双端队列 |
q.size() | 返回双端队列中的元素数量 |
q.empty() | 判断队列是否非空,为空返回 true,否则返回 false |
q.front() | 返回队列的队头元素 |
q.back() | 返回队列的队尾元素 |
q.clear() | 将队列清空 |
q.pop_front() | 从队头弹出一个元素 |
q.pop_back() | 从队尾弹出一个元素 |
q.push_front() | 从队头压入一个元素 |
q.push_back() | 从队尾压入一个元素 |
接下来我们手动模拟一个求区间最大值(区间长度为 )的单调队列:
数据:1 3 -1 -3 5 3 6 7。
进队,队列变为
。
进队,
,队列变为
。
进队,
,队列变为
。
进队,
,队列变为
。
的范围越界,队列变为
;
进队,队列变为
。
进队,
,队列变为
。
进队,队列变为
。
进队,
,队列变为
。
模拟结束。
如果这不够形象的话就看看题目提供的图吧:
单调队列在算法竞赛中主要有两个应用点:
1. 求解固定长度的区间最值。
2. 优化动态规划转移过程。
Q:单调队列是否适用于所有类型的数据?
A:不是的。单调队列主要能处理有这几种特点的数据:
1. 有序性。单调队列适用于具有潜在单调性趋势的数据序列。比如在滑动窗口问题中,数据是按照一定的顺序(如数组的索引顺序)排列的,并且窗口内的数据元素之间存在大小比较关系。如果数据是完全无序的,并且没有明显的顺序可以利用来建立单调性,那么单调队列就不太适用(也就是说你很难找到数据之间的比较方式)。
2. 数据规模较大。如果某道题,,要你求出某一段的区间最值。用暴力枚举就能做,那么用单调队列做就有点“大炮打蚊子——大材小用”的感觉了。
3. 最值查询问题。单调队列非常适合用于求解区间最值问题,如果是求和,求异或和,求最大公因数等其他方面的问题,单调队列就不太适用了(当然我没说不可以用)。
好了,回归正题,我们现在该解决的问题是:P1886 该怎么解决?
不是吧到了这里应该会写 P1886 了吧。
我们维护两个单调队列,一个用来维护区间最小值,一个用来维护区间最大值。
主要步骤如下:
1. 将已经超出区间范围的元素从队头弹出。
示例:
while(q.size()&&q.front()+m<=i){
q.pop_front();
}
3. 从队尾开始,如果队尾元素大于/小于当前元素的值,那么从队尾弹出元素:
示例(维护区间最小值):
while(q.size()&&a[q.back()]>=a[i]){
q.pop_back();
}
4. 从队尾压入当前元素:
示例:
q.push_back(i);
忠告:在查询/删除容器中的元素之前,一定要判断容器是否非空,否则在赛场上 RE 了别怪我没提醒你……而且建议大家在判断队头元素是否在区间范围内时,从减法判断转化为加法判断。因为有些题要开 unsigned long long 才能过,此时用减法判断就有可能出错。
实现
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m,t,a[1000005],X;
deque<int>qmin,qmax;
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=n;i++){
while(qmin.size()&&qmin.front()+m<=i){
qmin.pop_front();
}
while(qmin.size()&&a[qmin.back()]>=a[i]){
qmin.pop_back();
}
qmin.push_back(i);
if(i>=m){
cout<<a[qmin.front()]<<' ';
}
}
cout<<'\n';
for(int i=1;i<=n;i++){
while(qmax.size()&&qmax.front()+m<=i){
qmax.pop_front();
}
while(qmax.size()&&a[qmax.back()]<=a[i]){
qmax.pop_back();
}
qmax.push_back(i);
if(i>=m){
cout<<a[qmax.front()]<<' ';
}
}
return 0;
}
P8661 [蓝桥杯 2018 省 B] 日志统计
挺简单的,甚至都不需要比较……
我们先将点赞记录按照时间从小到大排序,然后遍历每个点赞记录。
如果点赞记录对应的帖子还不是“热帖”,那么我们先将对应的单调队列中的无用元素(时间超过限制)弹出,然后将新纪录压入单调队列中,如果此时单调队列中的元素大于等于 ,说明这个帖子成了“热帖”,标记答案。
实现
#include<bits/stdc++.h>
using namespace std;
int n,k,d;
deque<int>q[100005];
pair<int,int>a[100005];
bool ans[100005];
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>d>>k;
int maxid=0;
for(int i=1;i<=n;i++){
cin>>a[i].first>>a[i].second;
maxid=max(maxid,a[i].second);
}
sort(a+1,a+n+1);
for(int i=1;i<=n;i++){
if(!ans[a[i].second]){
while(q[a[i].second].size()&&a[i].first-q[a[i].second].front()>=d){
q[a[i].second].pop_front();
}
q[a[i].second].push_back(a[i].first);
if(q[a[i].second].size()>=k){
ans[a[i].second]=true;
}
}
}
for(int i=0;i<=maxid;i++){
if(ans[i]){
cout<<i<<'\n';
}
}
return 0;
}