滑动窗口——单调队列

单调队列其实就是维护一段大小确定区间内的最大最小值或者不知道其他什么需要单调性且区间大小恒定的的问题吧,感觉有个重要的问题就是本质上每个点都一定会入队一次的,大佬说:

举个例子,如果 OI 只注重第一名,而且有个神犇既比我小又比我强,那么我就可以退役了。因为我能参加的比赛他也能参加,而我又拿不到第一名,这时就可以把我弹出 OI 的队列了。

还有一点,就是如果某个人已经上大学了,那么即使他提高组能拿满分,他也参加不了,因为他年龄太大了,这时也要把他弹出。

luogu板子(https://www.luogu.com.cn/problem/P1886),就放啦:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m;
int a[1000001];
int q[1000001];
int p[1000001];
void findmax()
{	
	int l=1,r=1;
	for(int i=1;i<=n;i++)
	{
		while(l<=r&&q[l]+m<=i) l++;//先把前面超出范围m的数给它删掉 
		while(l<=r&&a[q[r]]<=a[i]) r--;//然后后面用不上的数也删掉 
		q[++r]=i;//插入进去咯 
		if(i>=m) printf("%lld ",a[q[l]]);
	}
	printf("\n");
	return ;
}
void findmin()
{
	int l=1,r=1;
	for(int i=1;i<=n;i++)
	{
		while(l<=r&&q[l]+m<=i) l++;
		while(l<=r&&a[q[r]]>=a[i]) r--;
		q[++r]=i;
		if(i>=m) printf("%lld ",a[q[l]]);
	}
	printf("\n");
	return ;
}
signed main()
{
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
	findmin();
	findmax();
	return 0;
}

给几道不知道什么东西的题:滑动窗口的 P1638 逛画展
思路的话呢,是先推右端点,然后当区间满足条件时候就推左端点,不过这个k用的确实不错,代码:

#include<bits/stdc++.h> 
using namespace std;
int n,m;
int ans=1e9; 
int a[1000001],b[2002];
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	int l=1,r=1,k=1,ll,rr;//l与r是推进区间的,然后k是判断能否满足所有大师 
	b[a[1]]=1;//帮它把第一个大师选了呗 
	while(l<=r&&r<=n)
	{
		if(k==m)//推进左端点 
		{
			if(ans>r-l+1) 
			{
				ans=r-l+1;//区间最小长度 
				ll=l,rr=r;
			}
			b[a[l]]--;
			if(b[a[l]]==0) k--;//如果减去后没有的话 
			l++;
		} 
		else //推进右端点 
		{
			r++;
			b[a[r]]++;
			if(b[a[r]]==1) k++;
		}
	}
	printf("%d %d",ll,rr);
	return 0;
}

又或者这道dp的优化,P5858 「SWTR-03」Golden Sword
做法直接看题解吧反正我是了,然后就直接单调队列优化一下就行:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,w,s;
int f[5505][5505];
int q[5505],pos[5505];
int a[5505];
main()
{
	scanf("%lld%lld%lld",&n,&m,&s);
	for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
	for(int i=0;i<=n;i++) 
	{
		for(int j=0;j<=m;j++) f[i][j]=-1e17;
	}
	f[0][0]=0;
	for(int i=1;i<=n;i++)
	{
		int l=1,r=1;
		q[l]=f[i-1][m];pos[l]=m;
		for(int j=m;j>=1;j--)
		{
			while(l<=r&&pos[l]>j+s-1) l++;
			while(l<=r&&q[r]<f[i-1][j-1]) r--;
			pos[++r]=j-1;q[r]=f[i-1][j-1];
			f[i][j]=q[l]+j*a[i];
		}
	}
	int ans=-1e17;
	for(int i=0;i<=m;i++) ans=max(ans,f[n][i]);
	printf("%lld",ans);
	return 0;
 } 

下一道也是单调队列的优化哦:P1725 琪露诺其实就是维护一个L,R的区间取其最大值然后递推啦:

#include<bits/stdc++.h>
using namespace std;
int n,m;
int L,R;
int q[1000001],a[210001];
int f[210001];
int main()
{//初值重要呀! 
	int ans=-1e9;
	scanf("%d%d%d",&n,&L,&R);
	for(int i=0;i<=n;i++) scanf("%d",&a[i]),f[i]=-1e9;
	f[0]=0;
	int l=1,r=1;
	for(int i=L;i<=n;i++)//说一下我的维护方式,就是前删前,去后,然后将点放入队列 
	{
		while(l<=r&&f[i-L]>f[q[r]]) r--;
		while(l<=r&&q[l]+R<i) l++;
		q[++r]=i-L;
		f[i]=f[q[l]]+a[i];
		if(i+R>n) ans=max(ans,f[i]); 
	}
	printf("%d",ans);
	return 0;
 } 

放一道单调栈的题目,P2422 良好的感觉 题外话感觉最近开心的要命草,不过不过得就是压制一下咯,得做题啦,其实题就不用维护区间长度所以就可以直接用单调栈来维护啦,其实就是维护一个点作为最小值时能向左右拓展多少啦,先维护点的左边,然后在后面点入栈的时候维护一下前面点的右边啦,代码来咯嘿嘿嘿:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m;
int f[100001],a[100001];
int q[100001];
int sum[100001];
signed main()
{
	scanf("%lld",&n);
	for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
	n++,a[n]=0;
	int l=0;
	for(int i=1;i<=n;i++)
	{	
		sum[i]=sum[i-1]+a[i];
		while(a[q[l]]>a[i])
		{
			f[q[l]]+=(sum[i-1]-sum[q[l]]);
			l--;
		}
		f[i]=sum[i]-sum[q[l]];
		q[++l]=i;
	}
	int ans=0;
	for(int i=1;i<=n-1;i++) ans=max(ans,f[i]*a[i]);
	printf("%lld",ans);
	return 0;
}

下一题:P2629 好消息,坏消息这是一道令我理解更进一步的题目,说的是先处理成链,然后算前缀和,然后呢,因为这个是要区间每一点都大于0嘛,所以可以用单调队列处理出最小值,然后用其减去断点就好啦:

#include<bits/stdc++.h>//不错的思路 
using namespace std;
int n,m;
int sum[2000001];
int a[2000001];
int q[100001];
int l=1,r=1,ans=0;
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]),a[i+n]=a[i];//处理成链	
	for(int i=1;i<=n*2-1;i++) sum[i]=sum[i-1]+a[i];//前缀和 
	for(int i=1;i<=n*2-1;i++)
	{
		while(l<=r&&max(i-n+1,1)>q[l]) l++;//如果这个最小值超出了其范围
		while(l<=r&&sum[i]<=sum[q[r]]) r--;
		q[++r]=i;
		if(i-n+1>0&&sum[q[l]]-sum[i-n]>=0) ans++;//统计啦 
	}
	printf("%d",ans);
	return 0;
}

P3522 [POI2011]TEM-Temperature这道的话挺有意思,就是要保证自己的最大值大于前面的最小值嘛,那就维护一个区间最小咯

#include<bits/stdc++.h>
using namespace std;
int n,m;
int ll[2000001],rr[2000001];
int q[2000001];//保存点
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d%d",&ll[i],&rr[i]);
	int len,ans=1,l=1,r=1;
	for(int i=1;i<=n;i++)
	{
		while(l<=r&&ll[q[l]]>rr[i]) l++;//如果我前面的那个超了我
		if(l<=r) len=i-q[l-1];//维护的长度,就是当前的这个到前面的那个不合法的长度
//		else len=1;
		ans=max(ans,len);
		while(l<=r&&ll[q[r]]<ll[i]) r--;//维护队列
		q[++r]=i;
	}
	printf("%d",ans);
	return 0;
 } 

下一道咯就是P1714 切蛋糕用单调队列,不想多说就是维护前缀和的最小值然后让自己的前缀和减去嘛

#include<bits/stdc++.h>
using namespace std;
int n,m,ans=0;
int q[100001];
int a[1000001],sum[1000001];
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]),sum[i]=sum[i-1]+a[i];
	int l=1,r=2;
	q[1]=0;
	for(int i=1;i<=n;i++)
	{
		while(l<=r&&i-m>q[l]) l++;
		while(l<=r&&sum[i]<sum[q[r]]) r--;
		ans=max(ans,sum[i]-sum[q[l]]);
		q[++r]=i;
	}
	printf("%d",ans);
	return 0;
}

下一个题目的话就是P3088 [USACO13NOV]Crowded Cows S 说真的这个不算难其实就是正做一遍反着再做一遍嗯嗯就这样啦:

#include<bits/stdc++.h>
using namespace std;
int n,m;
int q[1000000];
struct node
{
	int x,w,now;
}a[1000000];
bool v1[1000001],v2[100001];
bool cmp1(const node &x,const node &y)
{
	return x.w<y.w;
}
int main()
{
	memset(v1,true,sizeof(v1));
	memset(v2,true,sizeof(v2));
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d%d",&a[i].w,&a[i].x),a[i].now=i;
	sort(a+1,a+n+1,cmp1);
	int l=1,r=1;
	for(int i=1;i<=n;i++)
	{
		while(l<=r&&a[i].w-m>a[q[l]].w) l++;
		while(l<=r&&a[i].x>=a[q[r]].x) r--;
	
		q[++r]=i;
		if(a[q[l]].x>=2*a[i].x) v1[a[i].now]=false;
	}
	memset(q,0,sizeof(q));
	l=1,r=1;
	for(int i=n;i>=1;i--)
	{
		while(l<=r&&a[i].w+m<a[q[l]].w) l++;
		while(l<=r&&a[i].x>=a[q[r]].x) r--;
		q[++r]=i;
		if(a[q[l]].x>=a[i].x*2) v2[a[i].now]=false; 
	}
	int ans=0;
	for(int i=1;i<=n;i++)
	{
		if(v1[i]==false&&v2[i]==false) ans++;
	}
	printf("%d",ans);
	return 0;	
}

最后一题啦就是上面那个的加强版,就是那个逛画展,P2564 [SCOI2009]生日礼物 做法也差不多:

#include<bits/stdc++.h>
#define int long long 
using namespace std;
int n,m;
int k;
struct node
{
	int id,x;	
}a[2000000];
int b[200];
bool cmp(const node &x,const node &y)
{
	return x.x<y.x; 
}
int ans=1e18;
signed main()
{
	int tot=0;
	memset(b,0,sizeof(b));
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=m;i++)
	{
		int h;
		scanf("%lld",&h);
		for(int j=1;j<=h;j++) scanf("%lld",&a[++tot].x),a[tot].id=i;
	}
	sort(a+1,a+n+1,cmp);
	int l=1,r=1,k=1;b[a[1].id]=1;
	for(int i=2;i<=n;i++)
	{
		if(a[i-1].x==a[i].x) 
		{
			if(b[a[i].id]==0) k++;
			b[a[i].id]++;
			r++;
		}
		else break;
	}
	while(l<=r&&r<=n)
	{
		if(k==m)
		{
			ans=min(ans,a[r].x-a[l].x);
			b[a[l].id]--;
			if(b[a[l].id]==0) k--;
			l++;
			if(l>n) break;
			while(a[l].x==a[l-1].x)
			{
				b[a[l].id]--;
				if(b[a[l].id]==0) k--;
				l++;
				if(l>n) break;
			}
		}
		else 
		{
			r++;
			if(r>n) break;
			if(b[a[r].id]==0) k++;
			b[a[r].id]++;
			while(a[r].x==a[r+1].x) 
			{
				r++;
				if(r>n) break;
				if(b[a[r].id]==0) k++;
				b[a[r].id]++;
			}
		} 
	}
	printf("%lld",ans);
	return 0;
}

有一个用单调队列维护二分的题目P2698 [USACO12MAR]Flowerpot S,感觉很难,写下。草方向写反了草。

//这题一开始想错了,想直接用二分答案+单调队列枚举长度且直接比较做,但这样是错了,因为我只保存了一个区间的x最大值
//这样会导致y无法保证,正确的做法应该是将x排序后,枚举长度然后记录y的最大值与最小值。
#include<bits/stdc++.h>
using namespace std;
int n,m,ans=1e9;
struct node
{
	int x,y;
};node e[200001];
int minn[200001],maxx[200001];
bool check(int lim)
{
	int l1=1,r1=1,l2=1,r2=1,L=1;maxx[l1]=minn[l2]=1;
	for(int i=2;i<=n;i++)
	{
		while(l1<=r1&&e[i].x-e[maxx[l1]].x>lim) l1++;
		while(l2<=r2&&e[i].x-e[minn[l2]].x>lim) l2++;
		while(l1<=r1&&e[maxx[r1]].y<=e[i].y) r1--;
		while(l2<=r2&&e[minn[r2]].y>=e[i].y) r2--;
		minn[++r2]=i;	maxx[++r1]=i;
		if((e[maxx[l1]].y-e[minn[l2]].y)>=m) return true; 
	}
	return false;
}
bool cmp(const node &x,const node &y)
{
	if(x.x!=y.x) return x.x<y.x;
	return x.y<y.y;
}
int main()
{
	int l,r=2000010;scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d%d",&e[i].x,&e[i].y);
	sort(e+1,e+n+1,cmp);
	while(l<=r)
	{
		int mid=(l+r)/2;//printf("%d %d\n",l,r);
		if(check(mid)) r=mid-1,ans=min(ans,mid);
		else l=mid+1;
	}
	if(ans==1e9) printf("-1");
	else printf("%d",ans);
	return 0;
 } 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值