zoukankan      html  css  js  c++  java
  • 单调队列

    前言

    ​ 单调队列并不是太难的东西,不应其应用到的题目困难而觉得单调队列困难.

    ​ 我第一次遇见单调队列时是在学图论时,遇到了Island这道题(见基环树专题),当时的我对单调队列一无所知,而对其优化更是懵,所以当时就懵着将题解半抄半写地打了出来,但还是不懂.现在来看,单论单调队列,它是不难的,难的是与其他算法的结合;

    单调队列

    限制与应用

    ​ 对于单调队列,有两个操作,入队和出队;

    ​ 有两个限制,队首元素满足区间条件,队列中数满足单调;

    ​ 应用限制,转移DP时应满足区间取max-min操作,即让单调有其发挥作用的空间;

    入门

    ​ 推荐这道例题理解一下单调队列思想;

    老板需要你帮忙浇花。给出N滴水的坐标,y表示水滴的高度,x表示它下落到x轴的位置。

    每滴水以每秒1个单位长度的速度下落。你需要把花盆放在x轴上的某个位置,使得从被花盆接着的第1滴水开始,到被花盆接着的最后1滴水结束,之间的时间差至少为D。

    我们认为,只要水滴落到x轴上,与花盆的边沿对齐,就认为被接住。给出N滴水的坐标和D的大小,请算出最小的花盆的宽度W。

    ​ 我们首先考虑到答案是具有单调性的,宽度W满足条件,那么大于W的也会满足条件,那么我们可以二分答案;

    ​ 二分答案后,我们可以得到一个区间,我们想要的即所有区间中是否存在 (max-min>=D) ;

    ​ 这里就引入了单调队列,单调队列的队首表示区间 (i-mid-1) ~ (i) 最大值或者最小值,其限制条件即 (q[l]>=i-mid-1) ,不满足条件时弹出.

    ​ 入队时,比较队尾元素和要加入元素值,比较方式是 "比我小的人还比我强,那我就要退役了" ,即未来会更新到我们要加入值,存在时间肯定比队尾元素长,而队尾元素又比要加入的值小,那么队尾元素的贡献就可以被要加入的值代替,故直接弹出;

    ​ 那么这里我们限制一下区间,求出区间最大值与最小值,更新最大差值即可;

    #include<bits/stdc++.h>
    using namespace std;
    const int maxn=1e5+7,inf=1e9;
    int n,d,lim,q[maxn],f[maxn];
    struct node{
    	int x,y;
    }a[maxn];
    
    template<typename type_of_scan>
    inline void scan(type_of_scan &x){
    	type_of_scan 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;
    }
    
    bool check(int mid){
    	int l=1,r=0,x=1,y=0,ans=0;
    	for(int i=1;i<=n;i++){
    		int maxx=-1,minx=inf;
    		while(l<=r&&a[q[l]].x<=a[i].x-mid-1) l++;//限制区间弹出,注意区间大小为mid+1 
    		while(l<=r&&a[q[r]].y<=a[i].y) r--;//根据原则弹出 
    		q[++r]=i;maxx=max(a[q[l]].y,maxx);//更新,下面是一样的 
    		while(x<=y&&a[f[x]].x<=a[i].x-mid-1) x++;
    		while(x<=y&&a[f[y]].y>=a[i].y) y--;
    		f[++y]=i;minx=min(a[f[x]].y,minx);
    		ans=max(maxx-minx,ans);
    	}
    	return ans>=d;
    }
    
    bool cmp(node a,node b){
    	return a.x<b.x;
    }
    
    int main(){
    	scan(n),scan(d);
    	for(int i=1;i<=n;i++) scan(a[i].x),scan(a[i].y),lim=max(a[i].x,lim);
    	sort(a+1,a+1+n,cmp);//排序 
    	int l=1,r=lim;
    	while(r>l){
    		int mid=l+r>>1;
    		if(check(mid)) r=mid;
    		else l=mid+1;
    	}
    	if(!check(l)) puts("-1");
    	else printf("%d
    ",l);
    }
    

    ​ 另一道例题 ,主要是依靠单调队列原则进行;

    求一段最短的区间,其区间中包含了所有类型的颜色;

    ​ 根据题目中区间的信息,我们可以隐约地想到单调队列,但是并没有单调性.

    ​ 考虑队首弹出条件,当一个颜色在一个区间多次出现时,有贡献的只会是一个,那么当队首颜色重复时,其贡献已经被之后的颜色代替,也就没用了,那么既可以弹出;

    ​ 队尾弹出就不再需要了,我们直接更新满足条件的区间即可;

    ​ 那么思路即,对每种颜色计数,当队首元素颜色个数>=2时,将其弹出;

    #include<cstdio>
    #include<algorithm>
    using namespace std;
    int n,m,cnt,num;
    int q[1000007],ans=2147483647,vis[1000007];
    
    struct node{
    	int id,sp;	
    }a[1000007];
    
    bool operator <(node a,node b){
    	return a.sp<b.sp;
    }
    
    int main(){
    	scanf("%d%d",&n,&m);
    	for(int k=1,to;k<=m;k++){
    		scanf("%d",&to);
    		for(int i=1;i<=to;i++)
    			scanf("%d",&a[++cnt].sp),a[cnt].id=k;
    	}
    	sort(a+1,a+n+1);
    	int l=1,r=0;
    	for(int i=1;i<=n;i++){
    		if(vis[a[i].id]) vis[a[i].id]++;
    		if(!vis[a[i].id]) vis[a[i].id]++,num++;
    		while(vis[a[q[l]].id]>1) vis[a[q[l]].id]--,l++;//超过一个,弹出
    		q[++r]=i;
    		if(num==m) ans=min(ans,a[q[r]].sp-a[q[l]].sp);
    	}
    	printf("%d",ans);
    	return 0;
    }
    

    进阶

    ​ 二维单调队列,在一个固定矩形中,求其中最大值和最小值.

    例1

    [HAOI2007]理想的正方形

    在一个 (a*b) 的矩形中,求一个最大值与最小值差值最小的 (n*n) 的正方形,输出其差值;

    ​ 首先求出横向一维区间最大值,之后在原有横向区间最大值中求纵向一维区间最大值,这里由于用到了横向一维的值,使其变成了二维,那么思路就很显然了,注意区间大小和边界限制;

    #include<bits/stdc++.h>
    using namespace std;
    int a,b,n,maps[1007][1007],work1[1007][1007],qmax[1007],qmin[1007],ans1[1007][1007];
    int q[100007],work2[1007][1007],ans2[1007][1007],ans=2147483647;
    
    int main(){
    	scanf("%d%d%d",&a,&b,&n);
    	for(int i=1;i<=a;i++){
    		for(int j=1;j<=b;j++){
    			scanf("%d",&maps[i][j]);
    		}
    	}
    	for(int x=1;x<=a;x++){
    		int l=1,r=0,k=n;
    		memset(qmax,0,sizeof(qmax));
    		memset(qmin,0,sizeof qmin);
    		for(int i=1;i<=b;i++){
    			while(l<=r&&i-k>=qmax[l]) l++;
    			while(l<=r&&maps[x][qmax[r]]<=maps[x][i]) r--;
    			qmax[++r]=i;
    			if(i>=n) work1[x][i-k+1]=maps[x][qmax[l]];
    		}
    		l=1,r=0,k=n;
    		for(int i=1;i<=b;i++){
    			while(l<=r&&i-k>=qmin[l]) l++;
    			while(l<=r&&maps[x][qmin[r]]>=maps[x][i]) r--;
    			qmin[++r]=i;
    			if(i>=n) work2[x][i-k+1]=maps[x][qmin[l]];
    		}
    	}//横向区间最大最小值
    	for(int x=1;x<=b;x++){
    		int l=1,r=0,k=n;
    		memset(qmax,0,sizeof(qmax));
    		memset(qmin,0,sizeof qmin);
    		for(int i=1;i<=a;i++){
    			while(l<=r&&i-k>=qmax[l]) l++;
    			while(l<=r&&work1[qmax[r]][x]<=work1[i][x]) r--;
    			qmax[++r]=i;
    			if(i>=n) ans1[i-k+1][x]=work1[qmax[l]][x];
    		}
    		l=1,r=0,k=n;
    		for(int i=1;i<=a;i++){
    			while(l<=r&&i-k>=qmin[l]) l++;
    			while(l<=r&&work2[qmin[r]][x]>=work2[i][x]) r--;
    			qmin[++r]=i;
    			if(i>=n) ans2[i-k+1][x]=work2[qmin[l]][x];
    		}
    	}//纵向区间最大最小值
    	for(int i=1;i<=a-n+1;i++){
    		for(int j=1;j<=b-n+1;j++)
    			ans=min(ans,ans1[i][j]-ans2[i][j]);
    	}
    	printf("%d",ans);
    }
    

    例2

    [HAOI2007]修筑绿化带 ,

    如果把公园看成一个 (M * N) 的矩形,那么花坛可以看成一个 (C*D) 的矩形,绿化带和花坛一起可以看成一个 (A*B) 的矩形。

    如果将花园中的每一块土地的“肥沃度”定义为该块土地上每一个小块肥沃度之和,那么,

    绿化带的肥沃度= (A*B) 块的肥沃度 (- C*D)块的肥沃度

    为了使得绿化带的生长得旺盛,我们希望绿化带的肥沃度最大。

    震惊!某HA省竟在同一年考了两道相同算法的题

    ​ 这道题看起来是求最大区间值和,但是如果我们先用二维前缀和预处理出 (C*D) 矩形的权值和,然后再限制在 (A*B) 的矩形中,即上面那道题,有些不同的是,花坛不能触碰边界,注意边界的划分.

    #include<bits/stdc++.h>
    using namespace std;
    int m,n,c,d,a,b,maps[1007][1007],num[1007][1007];
    int qmin[1007],mms[1007][1007];
    int work2[1007][1007],ans2[1007][1007],ans;
    
    int main(){
    	scanf("%d%d%d%d%d%d",&m,&n,&a,&b,&c,&d);
    	for(int i=1;i<=m;i++){
    		for(int j=1;j<=n;j++){
    			scanf("%d",&maps[i][j]);
    			num[i][j]=maps[i][j]+num[i-1][j]+num[i][j-1]-num[i-1][j-1];
    		}
    	}
    	for(int i=c;i<=m;i++){
    		for(int j=d;j<=n;j++){
    			maps[i-c+1][j-d+1]=num[i][j]-num[i-c][j]-num[i][j-d]+num[i-c][j-d];
    			if(i>=a&&j>=b) mms[i-a+1][j-b+1]=num[i][j]-num[i-a][j]-num[i][j-b]+num[i-a][j-b];
    		}
    	}
    	for(int x=1;x<=m-1;x++){
    		int l=1,r=0,k=b-d-1;
    		memset(qmin,0,sizeof qmin);
    		for(int i=2;i<=n;i++){
    			while(l<=r&&i-k>=qmin[l]) l++;
    			while(l<=r&&maps[x][qmin[r]]>=maps[x][i]) r--;
    			qmin[++r]=i;
    			if(i-k+1>=0) work2[x][i-k+1]=maps[x][qmin[l]];
    		}
    	}
    	for(int x=1;x<=n-1;x++){
    		int l=1,r=0,k=a-c-1;
    		memset(qmin,0,sizeof qmin);
    		for(int i=1;i<=m;i++){
    			while(l<=r&&i-k>=qmin[l]) l++;
    			while(l<=r&&work2[qmin[r]][x]>=work2[i][x]) r--;
    			qmin[++r]=i;
    			if(i-k+1>0) ans2[i-k+1][x]=work2[qmin[l]][x];
    		}
    	}
    	for(int i=1;i<=m-a+1;i++){
    		for(int j=1;j<=n-b+1;j++){
    			ans=max(ans,mms[i][j]-ans2[i+1][j+1]);
    		}
    	}
    	printf("%d",ans);
    }
    

    DP与单调队列

    [SCOI2010]股票交易 ;

    通过一段时间的观察,( ext{lxhgww}) 预测到了未来 (T) 天内某只股票的走势,第 (i) 天的股票买入价为每股 (AP_i) ,第 (i) 天的股票卖出价为每股 (BP_i)(数据保证对于每个 (i),都有 (AP_i > BP_i) ),但是每天不能无限制地交易,于是股票交易所规定第 (i) 天的一次买入至多只能购买 (AS_i) 股,一次卖出至多只能卖出 (BS_i) 股。

    另外,股票交易所还制定了两个规定。为了避免大家疯狂交易,股票交易所规定在两次交易(某一天的买入或者卖出均算是一次交易)之间,至少要间隔 (W) 天,也就是说如果在第 (i) 天发生了交易,那么从第 (i+1) 天到第 (i+W) 天,均不能发生交易。同时,为了避免垄断,股票交易所还规定在任何时间,一个人的手里的股票数不能超过 ( ext{MaxP})

    在第 (1) 天之前,( ext{lxhgww}) 手里有一大笔钱(可以认为钱的数目无限),但是没有任何股票,当然,(T) 天以后,( ext{lxhgww}) 想要赚到最多的钱,聪明的程序员们,你们能帮助他吗?

    ​ 首先推出DP式子,设 (f[i][j]) 为第 (i) 天手持 (j) 个股票是的最大收入,那么分类转移;

    ​ CASE1: 没有买股票, (f[i][j]=f[i-1][j])

    ​ CASE2: 买了股票,但是有 (W) 天的限制,所以转移应为 (f[i][j]=max(f[i-w-1][j-k] - k*AP_i ,f[i][j])) ;

    ​ CASE3: 卖了股票, (f[i][j]=max(f[i-w-1][j+k]+k*BP_i , f[i][j])) ;

    ​ 那么 (k) 的枚举我们可以用单调队列优化掉,时间复杂度 (O(tm)) .

    #include<bits/stdc++.h>
    using namespace std;
    int t,m,w,f[2007][2007],q[2007],ans=-INT_MAX;
    int max(int a,int b){return a > b ? a : b;}
    
    int main(){
    	scanf("%d%d%d",&t,&m,&w);
    	memset(f,128,sizeof(f));//初始化最小值
    	for(int i=1,ap,bp,as,bs;i<=t;i++){
    		scanf("%d%d%d%d",&ap,&bp,&as,&bs);
    		memset(q,0,sizeof q);
    		for(int j=0;j<=as;j++) f[i][j]=-ap*j;
    		for(int j=0;j<=m;j++) f[i][j]=max(f[i][j],f[i-1][j]);
    		if(i<=w) continue;
    		int l=1,r=0;
    		for(int j=0;j<=m;j++){
    			while(l<=r&&q[l]<j-as) l++;
    			while(l<=r&&f[i-w-1][q[r]]+q[r]*ap<=f[i-w-1][j]+j*ap) r--;
    			q[++r]=j;
    			if(l<=r) f[i][j]=max(f[i][j],f[i-w-1][q[l]]+q[l]*ap-j*ap);
    		}//买的时候从比自己股票少的转移,正序枚举 
    		memset(q,0,sizeof q);
    		l=1,r=0;
    		for(int j=m;j>=0;j--){
    			while(l<=r&&q[l]>j+bs) l++;
    			while(l<=r&&f[i-w-1][q[r]]+q[r]*bp<=f[i-w-1][j]+j*bp) r--;
    			q[++r]=j;
    			if(l<=r) f[i][j]=max(f[i][j],f[i-1-w][q[l]]+q[l]*bp-j*bp);
    		}//卖的时候从比自己股票多的转移,倒序枚举 
    	}
    	printf("%d",f[t][0]);
    	return 0;
    } 
    

    总结

    ​ 单调队列可以有效处理区间最大最小值信息,这在DP方程转移过程中有重要意义,但在应用时应注意其应用范围,而不是盲目的套;

    ​ 应用时要注意细节问题,区间范围应卡好,出队时注意判断范围;

    练习题

    P2254 [NOI2005]瑰丽华尔兹 ;

    TO BE CONTINUE
  • 相关阅读:
    Java [leetcode 36]Valid Sudoku
    Java [leetcode 35]Search Insert Position
    java中正则表达式
    Java [leetcode 34]Search for a Range
    SSRS表达式里引用.net dll
    一个简单的批处理
    .NET大批量插入数据到Oracle
    AX2009里调用.NET DLL的效率问题
    生成折扣日记账
    python's twelth day for me
  • 原文地址:https://www.cnblogs.com/waterflower/p/11842527.html
Copyright © 2011-2022 走看看