浅谈单调栈/队列
Preface
其实我是真的不想写这个东西的,只不过做了一些这方面的水题,干脆写成一个专题
真的是水题,不毒瘤的PJ难度水题。我真是太菜了
思想简介
在维护一段区间的最值时,你一般会怎么做?
\(O(1)\)查询RMQ,或者是什么都能搞的线段树
如果只需要求一次呢?
还是RMQ/线段树
如果数据范围为\(10^7\) or more?
我们就可以用到玄学的单调栈/队列了。
首先相信栈和队列大家都不陌生吧,那么加了个单调上去意味着什么呢?
序列是单调的(废话),说明我们可以很快的找出最值
好像有这么一点道理,例如我们要求出一段序列的最小值
我们使一个队列里的元素单调递减,具体的:
对于每一个元素\(x\in q\),都有\(q_x>q_{x+1}\)
因此我们直接把队头拿出来就好了啊。
那么可能有人要问了,这TM不是直接开一个变量记录一下最小值就好的事情吗?
但是询问区间你左端点还是要向右弹出的啊!因此我们都要保留一个对未来可能有用的解。
只有当一对\(x,y\)元素满足\(x>y\)且\(a_x<a_y\)时\(y\)的存在才没有任何有意义。
复杂度是比较卓越的\(O(n)\),一般起辅助作用,常用来优化DP等算法。
不过我们今天不讲这么难的,我们只讲SB板子题。
POJ 2823 Sliding Window
题目大意:求一段长度为\(k\)的区间内的最小/大值。
这个单调队列板子题。上面已经讲的比较详细了,我们维护两个单调队列即可。
CODE
#include<cstdio>
#include<cctype>
const int N=1e6+5;
using namespace std;
int n,k,x;
inline char tc(void)
{
static char fl[100000],*A=fl,*B=fl;
return A==B&&(B=(A=fl)+fread(fl,1,100000,stdin),A==B)?EOF:*A++;
}
inline void read(int &x)
{
x=0; char ch; int flag=1; while (!isdigit(ch=tc())) flag=ch^'-'?1:-1;
while (x=(x<<3)+(x<<1)+ch-'0',isdigit(ch=tc())); x*=flag;
}
inline void write(int x)
{
if (x<0) putchar('-'),x=-x;
if (x>9) write(x/10); putchar(x%10+'0');
}
namespace min
{
int q[N],num[N],ans[N],head=1,tail,cnt;
inline void push(int x,int id)
{
while (tail>=head&&q[tail]>x) --tail;
q[++tail]=x; num[tail]=id;
}
inline void check(int now)
{
if (num[head]+k<=now) ++head;
ans[++cnt]=q[head];
}
};
namespace max
{
int q[N],num[N],ans[N],head=1,tail,cnt;
inline void push(int x,int id)
{
while (tail>=head&&q[tail]<x) --tail;
q[++tail]=x; num[tail]=id;
}
inline void check(int now)
{
if (num[head]+k<=now) ++head;
ans[++cnt]=q[head];
}
};
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
register int i; read(n); read(k);
for (i=1;i<=k;++i)
read(x),min::push(x,i),max::push(x,i);
min::check(k); max::check(k);
for (i=k+1;i<=n;++i)
read(x),min::push(x,i),max::push(x,i),min::check(i),max::check(i);
for (i=1;i<=min::cnt;++i)
write(min::ans[i]),putchar(i^min::cnt?' ':'\n');
for (i=1;i<=max::cnt;++i)
write(max::ans[i]),putchar(i^max::cnt?' ':'\n');
return 0;
}
Luogu P2422 良好的感觉
比较简单的套路题,拿来练一下RMQ也是挺好的。
我们对于每一个点,处理出它两端大于它的第一个位置的数
这样我们就可以算出当\(i\in [l,r],a_i=min(l,r)\)时每一个数的贡献。
这样保证不会遗漏。由于\(a_i>0\)。因此取的数越多越好
CODE
#include<cstdio>
#include<cctype>
const int N=1e5+5;
using namespace std;
int n,k,x,a[N],stack[N],front[N],back[N],top,num[N];
long long sum[N],ans;
inline char tc(void)
{
static char fl[100000],*A=fl,*B=fl;
return A==B&&(B=(A=fl)+fread(fl,1,100000,stdin),A==B)?EOF:*A++;
}
inline void read(int &x)
{
x=0; char ch; while (!isdigit(ch=tc()));
while (x=(x<<3)+(x<<1)+ch-'0',isdigit(ch=tc()));
}
inline long long max(long long a,long long b)
{
return a>b?a:b;
}
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
register int i; read(n);
for (i=1;i<=n;++i)
read(a[i]),sum[i]=sum[i-1]+a[i];
for (i=1;i<=n;++i)
{
while (top&&stack[top]>=a[i]) --top;
front[i]=num[top]; stack[++top]=a[i]; num[top]=i;
}
for (top=0,num[0]=n+1,i=n;i>=1;--i)
{
while (top&&stack[top]>=a[i]) --top;
back[i]=num[top]; stack[++top]=a[i]; num[top]=i;
}
for (i=1;i<=n;++i)
ans=max(ans,1LL*a[i]*(sum[back[i]-1]-sum[front[i]]));
return printf("%lld",ans),0;
}
Luogu P2629 好消息,坏消息
又是一道SB题,我们先将序列展开,方便操作。
即\(i\in[n+1,2n-1],a_i=a_{i-n}\),然后计算出前缀和
考虑每一次倒叙,就相当于求出一段长为\(n\)的区间的前缀和的最小值,并判断是否小于\(0\)
然后区间大小都固定了,上单调队列不是很爽吗?
CODE
#include<cstdio>
#include<cctype>
const int N=1e6+5;
using namespace std;
int n,k,x,a[N],q[N<<1],head=1,tail,num[N<<1],sum[N<<1],ans;
inline char tc(void)
{
static char fl[100000],*A=fl,*B=fl;
return A==B&&(B=(A=fl)+fread(fl,1,100000,stdin),A==B)?EOF:*A++;
}
inline void read(int &x)
{
x=0; char ch; int flag=1; while (!isdigit(ch=tc())) flag=ch^'-'?1:-1;
while (x=(x<<3)+(x<<1)+ch-'0',isdigit(ch=tc())); x*=flag;
}
inline void push(int x,int id)
{
while (tail>=head&&q[tail]>x) --tail;
q[++tail]=x; num[tail]=id;
}
inline int check(int now)
{
if (num[head]>now) ++head;
return q[head];
}
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
register int i; read(n);
for (i=1;i<=n;++i)
read(a[i]),sum[i]=sum[i-1]+a[i];
for (i=n+1;i<(n<<1);++i)
sum[i]=sum[i-1]+a[i-n];
for (i=(n<<1)-1;i>=n;--i)
push(sum[i],i);
for (i=n-1;i>=0;--i)
{
if (check(i+n)>=sum[i]) ++ans;
push(sum[i],i);
}
return printf("%d",ans),0;
}