题目大意
输入一个长度为n(n≤≤106106)的数列,给定一个长度为k的窗口,让这个窗口在数列上移动,求移动到每个位置窗口中包含数的最大值和最小值。即设序列为A1A1,A2A2,…,AnAn,设f(i)=min{Ai−k+1Ai−k+1,Ai−k+2Ai−k+2,…,AkAk} ,g(i)=max{Ai−k+1Ai−k+1,Ai−k+2Ai−k+2,…,AkAk}
求:f(k),f(k+1),…,f(n) g(k),g(k+1),…,g(n).
分析
本题算是单调队列的经典入门题。因此拿来学习单调队列。
如果单纯对每一个i都去扫描前面k个数去求最值,那复杂度为o(n*k),肯定超时。
如果用线段树或者树状数组维护区间最值,那复杂度为o(nlogn)。
但还有更简洁更高效的办法,即单调队列。
单调队列是一种特殊的队列,它能在队列两端进行删除操作,并始终维护队列保持一种单调性。
以求窗口最小值为例,我们需要维护一个内部元素值递减的队列来模拟滑动窗口,使每一次查询取队首元素即为答案。如何维护呢?有以下两种操作:
1.从队尾插入元素:当有新元素需要入队时,让它与当前队尾元素进行比较,若它小于等于当前队尾元素(即破坏了原队列的单调性),那么删除队尾元素,并继续比较队尾与新元素,直到找到一个队尾大于新元素时,将新元素插入到队尾。被删除的元素既比新元素大,又会比新元素先滑出窗口,因此肯定不会成为答案。这个操作不断维护了队列中的最值。
2.删除队首元素:由于序列中的元素当且仅当在滑动窗口时有效,因此,当队首元素不在滑动窗口内时,就删除队首元素。这个操作维护了数据的临时性。
元素是否过时与其下标有关,因此我队列中存储的是序列数对应的下标,这样每当要入队一个数,只需访问队首元素所存下标在序列中对应的数即可得到答案。
这样做尽管插入元素时有时需删除多个数,但每个数最多被删除一次,因此时间复杂度为O(n).
AC code
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1000000+5;
typedef struct Node{
int num;
int i;
}Node;
Node dq[2*maxn];
int n,m;
int num[maxn];
inline void get_max(int n,int m){//单调递减队列
for(int i=0;i<=n;i++)
dq[i].num=dq[i].i=0;
dq[0].num=0x3f3f3f3f;
int head=1,tail=1;
for(int i=1;i<=m;i++){
while(head<=tail && dq[tail].num<=num[i])
tail--;
tail++;
dq[tail].num=num[i];
dq[tail].i=i;
}
for(int i=m;i<=n;i++){
while(head<=tail && dq[tail].num<=num[i])
tail--;
tail++;
dq[tail].num=num[i];
dq[tail].i=i;
while(dq[head].i<=i-m)
head++;
cout<<dq[head].num<<' ';
}
cout<<endl;
}
inline void get_min(int n,int m){//单点递增队列
for(int i=0;i<=n;i++)
dq[i].num=dq[i].i=0;
dq[0].num=-0x3f3f3f3f;
int head=1,tail=1;
for(int i=1;i<=m;i++){
while(head<=tail && dq[tail].num>=num[i])
tail--;
tail++;
dq[tail].num=num[i];
dq[tail].i=i;
}
for(int i=m;i<=n;i++){
while(head<=tail && dq[tail].num>=num[i])
tail--;
tail++;
dq[tail].num=num[i];
dq[tail].i=i;
while(dq[head].i<=i-m)
head++;
cout<<dq[head].num<<' ';
}
cout<<endl;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>num[i];
}
get_min(n,m);
get_max(n,m);
return 0;
}