• 单调队列


    单调队列(小)总结

    最近,看到了自己洛谷任务计划队列中越堆越多,为了防止咕咕咕,我就来写一写单调队列,清一清计划。
    首先,我们先来看一道题: P1886 滑动窗口

    题目描述

    现在有一堆数字共N个数字(N<=10^6),以及一个大小为k的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。

    例如:

    The array is [1 3 -1 -3 5 3 6 7], and k = 3。

    输入格式

    输入一共有两行,第一行为n,k。

    第二行为n个数(x<INT_MAX)

    输出格式

    输出共两行,第一行为每次窗口滑动的最小值

    第二行为每次窗口滑动的最大值。

    输入输出样例

    8 3
    
    1 3 -1 -3 5 3 6 7
    
    -1 -3 -3 -3 3 3
    
    3 3 5 5 6 7
    

    数据范围

    50%的数据,(n<=10^5)

    100%的数据,(n<=10^6)


    这道题其实是属于单调队列的基础题,通过这道题就可以了解到单调队列。

    法1:暴力法

    我们可以考虑每一次滑动之后,我们枚举长度为k的序列,最后求出最大最小值。
    当然,这样的时间复杂度((n*k))是无法过掉这道题的。

    法2:单调队列

    我们可以来简单分析一下这个样例,这样更有助于我们理解单调队列。

    {1,3,-1,-3,5,3,6,7}

    首先,我们来看一下当k=3的时候的最小值。
    [1,3,-1] 此时,最小值为-1。
    [3,-1,-3] 此时,最小值为-3。
    [-1,-3,5] 此时,最小值为-3。
    我们可以发现,当加入比当前最小值大的数时,这个数是不对答案做出贡献的。
    所以,我们可以这样思考,
    对于一个空的队列来说,我们首先会先把(1)加入进来,
    然后再来看下一个数,我们发现3是有可能当做最优值的(就是当1不合法的时候),所以,我们就可以将3也加入到队列中。
    然后,下一个数(-1),我们发现-1<3,只要有-1存在时,3永远不可能作为最优解,所以我们就可以把3丢掉,(丢掉,全部丢掉),然后,看一下1,也是这个道理,所以我们就把1丢掉,然后再把-1加入队列中。
    对于每一次,我们都要去判断一下队头是否是符合条件的,如果不符合条件,那么就直接将其踢出。 (这里非常感谢zzh同学指明我上一次讲解的思路不足)

    所以,我们就可以将单调队列的基本规律写一下。

    加入操作时,我们需要将要加入的元素和队尾的元素进行比较,那个更优,如果当前的元素更优,那么就将队尾的元素踢出,一直踢到不能为止。
    我们再判断一下队首的元素满不满足条件,如果不满足的话就将队首的元素踢出。这样队列中就保持了 $$ 单调性 $$这一性质,我们就可以得到题目中的所要求的最值。
    下面,我们讲了这么多就可以直接上代码了(大家一定都会了)

    #include<bits/stdc++.h>
    using namespace std;
    const int maxn=1e6+7;
    int head,tail,p[maxn],q[maxn],i,n,a[maxn],k;
    void _min(){
    	head=1;
    	tail=0;
    	for(int i=1;i<=n;i++){
    		while(head<=tail&&p[tail]>=a[i]) tail--;//进行踢出处理 
    	p[++tail]=a[i];//将这个点加入队尾 
    	q[tail]=i;//标记这个点的位置 
    	if(q[head]<=i-k) ++head;
    	if(i>=k) printf("%d ",p[head]);
    	}
    }
    void _max(){
    	head=1;
    	tail=0;
    	for(int i=1;i<=n;i++){
    	while(head<=tail&&p[tail]<=a[i]) tail--;
    	p[++tail]=a[i];
    	q[tail]=i;
    	if(q[head]<=i-k) ++head;
    	if(i>=k) printf("%d ",p[head]); 	
    	}
    }
    
    int main(){
    	scanf("%d%d",&n,&k);
    	for(int i=1;i<=n;i++){
    		scanf("%d",&a[i]);
    	}
    	_min();
    	printf("
    ");
    	_max();
    	return 0;
    }
    

    这道题算是属于单调队列的入门题目,也可以帮助大家理解单调队列的原理。


    下面我们来看下一道例题,是一道使用单调队列优化dp

    P2569 [SCOI2010]股票交易

    我们来总结一下这道题的题意(算是一句话题意吧):
    我们现在知道了这一段时间(T)内的股票的买入((AP_i))卖出((BP_i))价和最多买入((AS_i))卖出((BS_i))的数量限制,以及我们最多可以有(MAXP)支股票和交易之间需要间隔的时间(W)

    首先

    我们可以很简单的看出来这是一道dp题,而且动态规划方程也不是很难想,状态的话我们可以这样考虑,因为数据范围中是(leq 2000),所以,我们完全可以开出一个2000*2000的数组。所以,我们第一维就很理所当然的表示出时间。第二维的话我们就可以将拿的股票数作为状态。这样我们就可以将状态列了出来(f[i][j])表示在前(i)天有(j)支股票时的最大收益。

    现在我们来考虑一下如何将状态进行转移

    Case 1:

    在什么都没有的情况下直接进行购买(又称,裸买)其余的状态赋为(-inf)

    [f[i][j]=-1*ap[i]*j ]

    Case 2:

    不买也不卖的情况下,我们可以直接进行转移。

    [f[i][j]=max(f[i-1][j-1],f[i][j]) ]

    Case 3:

    这里是最麻烦的一部分了,(菜鸡请自行离开,对,就是我),我们这里就是在我们买过的基础上再去买票。
    首先,我们要去考虑一下题目上最前面的那个(W)天,我们知道每两次交易的时间要间隔(W)天,所以我们在更新的时候上一次交易的时间间隔就是在(i-w-1)的时候。有的人可能会问了,为什么不会是(i-2-w)
    而偏偏是(i-1-w)(因为你傻) 这是因为如果是(i-2-w)的话我们会在前面的时候进行更新,所以我们就不会有这个问题。
    我们就可以设(i-1-w)天时有k支股票,因为有限制最多(as[i])的限制,所以我们就可以把这个转移方程列了出来

    [f[i][j]=max(f[i][j],f[i-w-1][k]-(j-k)*ap[i]) ]

    [(j-as[i]leqslant k < j) ]

    Case 4:

    解决完在原有情况下的买入,我们就可以类比一下很快解决出卖出的状态转移方程。

    [f[i][j]=max(f[i][j],f[i-w-1][k]+(k-j)*bp[i]) ]

    [(j < k geqslant j+bs[i]) ]

    这样我们就把最基础的状态转移方程推了出来

    这时,我们发现整个转移下来所需要的时间复杂度是三次方的,这明显是过不了的,我们就可以考虑使用单调队列来进行优化。
    我们可以来看一下这个最初的状态转移方程,

    [f[i][j]=max(f[i][j],f[i-w-1][k]-(j-k)*ap[i]) ]

    [(j-as[i]leqslant k < j) ]

    我们来拆分一下

    [f[i][j]=max(f[i][j],f[i-w-1][k]-j*ap[i]+k*ap[i]) ]

    因为我们要转移(f[i][j]) 所以,我们可以将(j)提出来,这样我们的方程就可以转化为

    [f[i][j]=max(f[i][j],f[i-w-1][k]+k*ap[i])-j*ap[i] ]

    这个时候,我们就可以发现我们找到了在转移的时候我们要取到最大值,所以,这个时候我们就可以使用单调队列来进行优化。
    同理,我们对于情况4也一样适用的,我们可以将转移方程转化一下,

    [f[i][j]=max(f[i][j],f[i-w-1][k]+k*bp[i])-j*bp[i] ]

    所以,我们就可以使用单调队列来进行转移了。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<cmath>
    using namespace std;
    template<typename type>
    void scan(type &x){
    	type f=1;x=0;char s=getchar();
    	while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
    	while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    	x*=f;
    }
    #define itn int
    const int N=2007;
    int n,maxp,w,as,ap,bs,bp,f[N][N],qu[N];
    int l,r,ans=0;
    
    int main(){
    	scan(n);scan(maxp);scan(w);
    	memset(f,128,sizeof(f));//当赋值为127时,此时赋值的是最大值,当为128时,此时赋值时最小值
    	for(int i=1;i<=n;i++){
    		scan(ap);scan(bp);scan(as);scan(bs);
    		for(int j=0;j<=as;j++){
    			f[i][j]=-1*j*ap;//情况1
    		}
    		for(int j=0;j<=maxp;j++){
    			f[i][j]=max(f[i][j],f[i-1][j]);//情况2
    		}
    		if(i<=w)continue;
    		l=1;r=0;
    		for(int j=0;j<=maxp;j++){
    			while(l<=r&&qu[l]<j-as)l++;//踢出不合法的选项
    			while(l<=r&&f[i-w-1][qu[r]]+qu[r]*ap<=f[i-w-1][j]+j*ap)r--;
    			qu[++r]=j;
    			if(l<=r)
    				f[i][j]=max(f[i][j],f[i-1-w][qu[l]]+qu[l]*ap-j*ap);//如果队列中有符合条件的数值,我们就可以将方程进行转移
    		}
    
    		l=1,r=0;
    		for(int j=maxp;j>=0;j--){
    			while(l<=r&&qu[l]>j+bs)l++;
    			while(l<=r&&f[i-w-1][qu[r]]+qu[r]*bp<=f[i-w-1][j]+j*bp)r--;
    			qu[++r]=j;//同理
    			if(l<=r){
    				f[i][j]=max(f[i][j],f[i-w-1][qu[l]]+qu[l]*bp-j*bp);
    			}
    		}
    
    	}
    	for(int i=0;i<=maxp;i++){
    		ans=max(ans,f[n][i]);
    	}
    	printf("%d
    ",ans);
    	return 0;
    }
    
    

    这道题我们就可以是使用单调队列成功的优化一下dp从而切掉。_


    下面,我们来看一道省选题(二维单调队列)

    P2216 [HAOI2007]理想的正方形

    一句话题意:有一个(a*b)的整数组成的矩阵,现请你从中找出一个(n*n)的正方形区域,使得该区域所有数中的最大值和最小值的差最小。

    题目分析:

    我们不难发现,这个时候,我们不仅要维护一个最大的信息,我们还要维护一个最小的信息。所以我们就要进行两次的单调队列的转移,这样我们就可以

    #include<cstdio>
    #include<iostream>
    #include<cstring>
    #include<cmath>
    #include<algorithm>
    using namespace std;
    template <typename type>
    void scan(type &x){
     	type f=1;x=0;char s=getchar();
     	while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
    	while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    	x*=f;
    }
    const int N=1e3+8;
    #define itn int
    int n,m,k,ans=1e9;
    int a[N][N],p[N],q[N];
    int x[N][N],y[N][N];
    int xx[N][N],yy[N][N];
    void init(){
    	memset(p,0,sizeof(p));
     	memset(q,0,sizeof(q));
    }
    void work1(itn id){//处理列
    	int l=1,r=0;
    	for(int i=1;i<=m;i++){
    		while(l<=r&&p[r]<=a[id][i])//取最大值
    		r--;
    		p[++r]=a[id][i];
    		q[r]=i;
    		while(q[l]<=i-k)l++;//不满足选的条件的话,将左边舍去 
    		if(i>=k)x[id][i-k+1]=p[l];//在行确定的时候,在满足题目条件的情况下,统计每一列的答案
    	}
    	init();//清空
    	l=1;r=0;
    	for(int i=1;i<=m;i++){
    		while(l<=r&&p[r]>=a[id][i])
    		r--;
    		p[++r]=a[id][i];
    		q[r]=i;
    		while(q[l]<=i-k)l++;
    		if(i>=k)xx[id][i-k+1]=p[l];
    	}
    }
    void work2(itn id){
    	int l=1,r=0;
    	for(int i=1;i<=n;i++){
    		while(l<=r&&p[r]<=x[i][id])r--;
    		p[++r]=x[i][id];
    		q[r]=i;
    		while(q[l]<=i-k)l++;
    		if(i>=k)y[i-k+1][id]=p[l];
    	}
    	init();
    	l=1,r=0;
    	for(itn i=1;i<=n;i++){
    		while(l<=r&&p[r]>=xx[i][id])r--;
    		p[++r]=xx[i][id];
    		q[r]=i;
    		while(q[l]<=i-k)l++;
    		if(i>=k)yy[i-k+1][id]=p[l];
    	}
    }
    
    int main(){
    //	freopen(".in","r",stdin);
    //	freopen(".out","w",stdout);
    	scan(n);scan(m);scan(k);
    	for(int i=1;i<=n;i++){
    		for(int j=1;j<=m;j++){
    			scan(a[i][j]);
    		}
    	}
    	for(int i=1;i<=n;i++)work1(i);
    	for(int j=1;j<=m;j++)work2(j);
    	for(int i=1;i<=n-k+1;i++){
    		for(int j=1;j<=m-k+1;j++){
    //			printf("%d %d
    ",y[i][j],yy[i][j]);
    			ans=min(ans,y[i][j]-yy[i][j]);
    		}
    	}
    	printf("%d
    ",ans);
    	return 0;
    }
    

    下一道题:

    P2219 [HAOI2007]修筑绿化带

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<cmath>
    #include<algorithm>
    using namespace std;
    template <typename type>
    void scan(type &x){
    	type f=1;x=0;char s=getchar();
    	while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
    	while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    	x*=f;
    }
    #define itn int
    const int N=2007;
    int m,n,a,b,c,d,ans=0;
    int gra[N][N],x[N][N],y[N][N],flo[N][N];
    int s[N][N],q[N],p[N];
    void init(){
    	memset(p,0,sizeof(p));
    	memset(q,0,sizeof(q));
    }
    void work1(int id){
    	init();
    	int l=1,r=0,len=b-d-1;
    	for(int i=1;i<=m;i++){
    		while(l<=r&&p[r]>=flo[id][i])r--;
    		p[++r]=flo[id][i];
    		q[r]=i;
    		while(q[l]<=i-len)l++;
    		if(i>=len)x[id][i-len+1]=p[l];
    	}
    }
    void work2(int id){
    	init();
    	int l=1,r=0,len=a-c-1;
    	for(int i=1;i<=n;i++){
    		while(l<=r&&p[r]>=x[i][id])r--;
    		p[++r]=x[i][id];
    		q[r]=i;
    		while(q[l]<=i-len)l++;
    		if(i>=len)y[i-len][id-1]=p[l];
    	}
    }
    
    
    int main(){
    //	freopen(".in","r",stdin);
    //	freopen(".out","w",stdout);
    	scan(n);scan(m);scan(a);scan(b);scan(c);scan(d);
    	for(int i=1;i<=n;i++){
    		for(int j=1;j<=m;j++){
    			scan(s[i][j]);
    			s[i][j]+=s[i-1][j]+s[i][j-1]-s[i-1][j-1];
    		}
    	}
    	for(int i=1;i<=n-a+1;i++){
    		for(itn j=1;j<=m-b+1;j++){
    			gra[i][j]=s[i+a-1][j+b-1]-s[i+a-1][j-1]-s[i-1][j+b-1]+s[i-1][j-1];
    		}
    	}
    	for(int i=1;i<=n-c+1;i++){
    		for(itn j=1;j<=m-d+1;j++){
    			flo[i][j]=s[i+c-1][j+d-1]-s[i+c-1][j-1]-s[i-1][j+d-1]+s[i-1][j-1];
    		}
    	}
    	for(int i=1;i<=n;i++)work1(i);
    	for(itn i=1;i<=m;i++)work2(i);
    	for(itn i=1;i<=n-a+1;i++){
    		for(int j=1;j<=m-b+1;j++){
    			ans=max(ans,gra[i][j]-y[i][j]);
    			
    		}
    	}
    	printf("%d
    ",ans);
    	return 0;
    }
    

    是我太懒了,所以后半部分就先咕到这里吧。

  • 相关阅读:
    Django+xadmin打造在线教育平台(八)
    Django+xadmin打造在线教育平台(七)
    Django+xadmin打造在线教育平台(六)
    Django+xadmin打造在线教育平台(五)
    Django+xadmin打造在线教育平台(四)
    Django+xadmin打造在线教育平台(三)
    Django+xadmin打造在线教育平台(二)
    Cognos组织架构介绍
    Echarts 的悬浮框tooltip显示自定义格式化
    Echarts中axislabel文字过长导致显示不全或重叠
  • 原文地址:https://www.cnblogs.com/xishirujin/p/11716246.html
Copyright © 2020-2023  润新知