191103单调队列

定义:

1、维护区间最值;
2、去除冗杂状态;
3、保持队列单调(最大值是单调递减序列,最小值是单调递增序列);
4、最优选择在队首。

单调队列的使用方法:

1、维护队首;
2、在队尾插入(每插入一个就要从队尾开始往前去除冗杂状态) ;
3、取出需要的最优解(队列头的值即是),借助最优解,得到目前所求的最优解(通常此处插入DP方程)。

单调队列的原理:

在处理f[i]时,去除冗杂、多余的状态,使得每个状态在队列中只会出现一次;同时维护一个能瞬间得出最优解的队列,减少重新访问的时间;在取得自己所需的值后,为后续的求解做好准备。

使用条件:

1.涉及到维护区间最值;
2.区间出现平移

模板(维护递增):
	while(head<=tail&&是否位于所求区间)	++head;  //保证题目的限制 
	while(head<=tail&&a[i]>a[q[tail]])	--tail;  //维护单调性,不符合要求的出队 
	q[++tail]=i;	//入队 

我们对每个i执行以下三个步骤:
(1)判断队列队首枚举的 j ( q [ l ] ) j(q[l]) jq[l] i i i的距离是否超出 M M M的范围,若超出则队首出队。
(2)此时队首就是右端点为i时,左端点j的最优选择。
(3)不断删除队尾枚举的 j ( q [ r ] ) j(q[r]) jq[r],直到队尾对应的 S S S S [ j ] S[j] S[j]小于 S [ i ] S[i] S[i]。然后把 i i i作为一个 j j j新的决策入队。

例题:
T1:志愿者选拔

传送门

就是一道模板题,但是这道题的数据卡常卡的我作呕,必须加输入输出优化,不能用 s c a n f 和 p r i n t f scanf和printf scanfprintf,必须用 g e t s 和 p u t s gets和puts getsputs

维护一个单调递减区间,询问时输出队首即可,有人离开考场,就 + + l a s t ++last ++last,之后对于队列中所有超出范围的前端元素进行出
队操作。

这里考虑一个问题,当前元素加入后对队尾元素为什么可以毫无保留的删去呢? 因为当前加入的元素比原来的队尾元素大,且该元素比原队尾元素入队晚(也就是该元素比原队尾元素晚出队),所以只要该元素在队列中,就一定不会选取原队尾元素。也就是当前状态一定比原队尾元素的状态更优。——这里一定要理解深刻,这是队列的本质。
因此,这题的单调队列中维护的一个属性是元素的价值,一个属性是单调队列中的元素在原序列中的位置。
注意,q中的值是该元素在原序列中的位置

代码:

#include<bits/stdc++.h>
#define ll long long
#define db double
#define re register
#define cs const
using namespace std;
inline int read()
{
	int x=0,f=1;
	char ch;
	while(ch>'9'||ch<'0')
	{
		if(ch=='-')	f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=x*10+ch-'0';
		ch=getchar();
	}
	return f*x;
}
int T,num[1000005];
int q[1000005],head=1,tail=0,r=0;
char ch[50],w;
void print(int n)
{
	if(n>9) print(n/10);
	putchar(n%10+48);
}
int main()
{
	//freopen("a.in","r",stdin);
	T=read();
	while(T--)
	{
		r=0;
		head=1;
		tail=0;
		int last=0;
		memset(q,0,sizeof(q));
		gets(ch);
		while(w=getchar(),w!='E')
		{
			if(w=='C')
			{
				q[++r]=read();
				while(q[r]>q[num[tail]]&&head<=tail)	--tail;
				num[++tail]=r;
			}
			if(w=='G')
			{
				++last;
				while(head<=tail&&last>=num[head])	++head;
			}
			if(w=='Q')
			{
				if(head>tail)	puts("-1");
				else	print(q[num[head]]),putchar('\n');
			}
		}
	}
}

T2:广告印刷

传送门

我们可以很容易地看出最后答案一定是某个建筑的高度乘上它的最长长度,所以我们维护一个单调递增队列,若遇到小于队尾的值,则队尾元素出队,更新其右极限值;而求左极限,同理,从 n n n 0 0 0扫一遍即可。

注意:求右极限值时要扫到 n + 1 n+1 n+1,使得所有元素出入队一次,

代码:

#include<bits/stdc++.h>
#define int long long
#define db double
#define re register
#define cs const
#define N 1000005
using namespace std;
inline int read()
{
	int x=0,f=1;
	char ch;
	while(ch>'9'||ch<'0')
	{
		if(ch=='-')	f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=x*10+ch-'0';
		ch=getchar();
	}
	return f*x;
}
int n,h[N],l[N],r[N],q[N],head=1,tail=0;
signed main()
{
	n=read();
	for(re int i=1;i<=n;++i)	h[i]=read();
	h[0]=h[n+1]=0;
	for(re int i=1;i<=n+1;++i)
	{
		while(h[i]<h[q[tail]]&&head<=tail)
		{
			r[q[tail]]=i;
			--tail;
		}
		q[++tail]=i;
	}
	head=1;
	tail=0;
	memset(q,0,sizeof(q));
	for(re int i=n;i>=0;--i)
	{
		while(h[i]<h[q[tail]]&&head<=tail)
		{
			l[q[tail]]=i;
			--tail;
		}
		q[++tail]=i;
	}
	int ans=0;
	for(re int i=1;i<=n;++i)	ans=max(ans,(r[i]-l[i]-1)*h[i]);
	printf("%lld",ans);
}

T3:滑动的窗户

传送门

这道应该才是大多数人第一次接触单调队列时的题,我们同样维护两个队列,一个单调递增,一个单调递减,而这道题的限制条件变成了队列长度是否大于 k k k

代码:

#include<bits/stdc++.h>
#define ll long long
#define db double
#define re register
#define cs const
#define N 1000005
using namespace std;
inline int read()
{
	int x=0,f=1;
	char ch;
	while(ch>'9'||ch<'0')
	{
		if(ch=='-')	f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=x*10+ch-'0';
		ch=getchar();
	}
	return f*x;
}
int l1=1,l2=1,r1,r2,q1[N],q2[N],ans1[N],ans2[N];
int n,k,a[N];
void print(int x)
{
	if(x<0)	putchar('-'),x=-x;
	if(x>9)	print(x/10);
	putchar(x%10+48);
}
int main()
{
	n=read();
	k=read();
	for(re int i=1;i<=n;++i)	a[i]=read();
	for(re int i=1;i<=n;++i)
	{
		while(l1<=r1&&q1[l1]<=i-k)	++l1;
		while(l2<=r2&&q2[l2]<=i-k)	++l2;
		while(a[i]>a[q1[r1]]&&l1<=r1)	--r1;
		q1[++r1]=i;
		while(a[i]<a[q2[r2]]&&l2<=r2)	--r2;
		q2[++r2]=i;	
		ans1[i]=a[q1[l1]];
		ans2[i]=a[q2[l2]];
	}
	for(re int i=k;i<=n;++i)	print(ans2[i]),putchar(' ');
	putchar('\n');
	for(re int i=k;i<=n;++i)	print(ans1[i]),putchar(' ');
}

T4:最长连续子段和

传送门

一般这种序列和的题,我们可以转化成前缀和,因此此题可以转化成:找到 m a x ( s u m [ j ] − s u m [ i ] ) max(sum[j]-sum[i]) max(sum[j]sum[i]) ( j − i ) < = m (j-i)<=m (ji)<=m,于是我们可以枚举右端点,找到其取最大值时的左端点,即 s u m [ i ] sum[i] sum[i]最小,与 a n s ans ans m a x max max即可,再用单调队列维护单调递增即可。

代码:

#include<bits/stdc++.h>
#define int long long
#define db double
#define re register
#define cs const
#define N 1000005
using namespace std;
inline int read()
{
	int x=0,f=1;
	char ch;
	while(ch>'9'||ch<'0')
	{
		if(ch=='-')	f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=x*10+ch-'0';
		ch=getchar();
	}
	return f*x;
}
int n,m,a[N],ans,sum[N];
int head=1,tail=0,q[N];
signed main()
{
	n=read();
	m=read();
	for(re int i=1;i<=n;++i)
	{
		a[i]=read();
		sum[i]=sum[i-1]+a[i];
	}
	for(re int i=1;i<=n;++i)
	{
		while(head<=tail&&q[head]<i-m)	++head;
		ans=max(ans,sum[i]-sum[q[head]]);
		while(head<=tail&&sum[i]<sum[q[tail]])	--tail;
		q[++tail]=i;
	}
	printf("%lld",ans);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值