浅谈单调栈/队列

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;
}
posted @ 2018-08-03 22:14  空気力学の詩  阅读(218)  评论(0编辑  收藏  举报