单调队列—学习笔记

Q:
给定一个长度为N的序列(可能有负数),
从中找出一段长度不超过M的连续子序列,使得其和最大
N ≤ 500000 N≤500000 N500000

A:
对于这题
首先不难想到先求出数列前缀和sum[]
那么显然问题的答案就是 m a x i = m n (   s u m [ i ] − m i n j = i − m i − 1 ( s u m [ j ] )   ) max_{i=m}^n(\ sum[i]-min_{j=i-m}^{i-1}(sum[j])\ ) maxi=mn( sum[i]minj=imi1(sum[j]) )

通俗的说
我们需要维护整个数列中所有长度为m的连续子序列的最小值

线段树??ST表???
虽然都是nlogn的算法,但对于这题显然还是略显吃力
所以这时考虑引入我们的单调队列

单调队列需要一个双端队列实现
可以直接维护每个长度为m的连续子数列的最小值

具体怎么做呢
我们从第一个元素一次往后遍历整个序列,每遍历一个元素
1.检查当前队列中队首元素位置距当前元素是否超过m,是则不断从队首弹出直到小于m个
2.检查队尾元素是否大于等于当前元素,是则不断从队尾弹出元素,
直到队列为空队尾元素小于当前元素
3.将当前元素入队
此时队首保存的就是从当前元素开始往前m个元素中的最小值了
整个算法复杂度只有 O ( N ) O(N) O(N)

当然,我们在实际操作时
队列中可以只保存编号,进行2操作时只要映射到原序列对应元素
这样就可以省去用结构体同时储存值与编号的麻烦


入门题集

洛谷P1714 切蛋糕

就是上面的问题

#include
#include
#include
#include
#include
#include
using namespace std;

int read()
{
    int f=1,x=0;
    char ss=getchar();
    while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
    while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
    return f*x;
}

const int maxn=500010;
int n,m;
int sum[maxn];
int q[maxn],ll=1,rr=1;
int ans=-1e9;

int main()
{
    n=read();m=read();
    for(int i=1;i<=n;i++)
    sum[i]=read(),sum[i]+=sum[i-1];
    
    for(int i=1;i<=n;i++)
    {
        while(ll<rr&&q[ll]<i-m) ll++;//检查队列中元素个数
        ans=max(ans,sum[i]-sum[q[ll]]);
        //此题中查询最小值不包含当前元素,所以入队前先更新
        while(ll<rr&&sum[i]<=sum[q[rr-1]]) rr--;//检查队尾元素与当前元素大小
        q[rr++]=i;//当前元素入队
    }
    printf("%d",ans);
    return 0;
}

洛谷P1725 琪露诺

有n+1个格子,起点在格子0,每个格子有一个数值,每次能向编号大的格子跳,跳跃距离为[L,R],求能得到的最大分值总和

d p [ i ] dp[i] dp[i]表示跳到 i i i能获得的最大分数, d p [ i ] = d p [ j ] + A [ i ] ( i − r ≤ j ≤ i − l ) dp[i]=dp[j]+A[i](i-r\leq j\leq i-l) dp[i]=dp[j]+A[i](irjil)
单调队列维护的区间与当前位置不连续

ll=rr=1; dp[0]=a[0];
for(int i=L;i<=n;++i)
{
    while(ll<rr&&dp[q[rr-1]]<=dp[i-L]) rr--;
    q[rr++]=i-L;
    while(ll<rr&&q[ll]<i-R) ll++;
    dp[i]=dp[q[ll]]+a[i];
    if(i>=n-R) ans=max(ans,dp[i]);
}
拓展

在上述问题的基础上,若给定的格子位置不连续(pos[i]为第i个格子的位置)

显然每次能转移到pos[i]的合法区间会随着i的增加而增加(即转移区间有单调性)
可以用一个指针p维护距离当前位置大于等于L的最近的位置,即先让队内元素满足 j ≤ i − l j\leq i-l jil
再不断弹出队首不满足 i − r ≤ j i-r\leq j irj的位置

语死早,不太会描述,看看代码意会一下吧

int p=0; ll=rr=1;
memset(dp,128,sizeof(dp)); dp[0]=a[0];
for(int i=1;i<=n;++i)
{
    while(pos[i]-pos[p]>=L&&p<i)
    {while(dp[q[rr-1]]<=dp[p]&&ll<rr)--rr; q[rr++]=p++;}
        
    while(pos[i]-pos[q[ll]]>R&&ll<rr)++ll;
    if(ll>=rr||dp[q[ll]]==-inf) continue; //没有合法的位置可以转移
    dp[i]=dp[q[ll]]+a[i]; 
    if(i>=n-R) ans=max(ans,dp[i]);
}

洛谷P3512 [POI2010]PIL-Pilots

给定n,k和一个长度为n的序列,求最长的最大值最小值相差不超过k的序列

二分序列长度mid,单调队列找每个长度为mid的子序列的最小/大值即可

#include
#include
#include
#include
#include
#include
using namespace std;

int read()
{
    int f=1,x=0;
    char ss=getchar();
    while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
    while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
    return f*x;
}

const int maxn=3000010;
int n,k;
int a[maxn];
int q[2][maxn],ll[2],rr[2];
int ans;

int check(int m)
{
    ll[0]=ll[1]=rr[0]=rr[1]=1; 
    q[0][1]=q[1][1]=0;
    for(int i=1;i<=n;++i)
    {
        while(ll[0]<rr[0]&&q[0][ll[0]]<i-m+1)++ll[0];
        while(ll[0]<rr[0]&&a[q[0][rr[0]-1]]>=a[i])--rr[0];
        
        while(ll[1]<rr[1]&&q[1][ll[1]]<i-m+1)++ll[1];
        while(ll[1]<rr[1]&&a[q[1][rr[1]-1]]<=a[i])--rr[1];
        
        q[0][rr[0]++]=i; q[1][rr[1]++]=i;
        if(i>=m&&a[q[1][ll[1]]]-a[q[0][ll[0]]]<=k)return 1;
    }
    return 0;
}

int main()
{
    k=read();n=read();
    for(int i=1;i<=n;++i)a[i]=read();
    
    int L=1,R=n,mid;
    while(L<R)
    {
        mid=L+R>>1;
        if(check(mid))ans=mid,L=mid+1;
        else R=mid;
    }
    if(check(L))ans=L;
    printf("%d",ans);
    return 0;
}

洛谷P2698 [USACO12MAR]花盆Flowerpot

一样二分花盆宽度,单调队列检查该长度区间内的y坐标最大/小值差值

#include
#include
#include
#include
#include
#include
using namespace std;
typedef long long lt;

int read()
{
    int f=1,x=0;
    char ss=getchar();
    while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
    while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
    return f*x;
}

const int maxn=100010;
int n,D,mx;
struct node{int x,y;}p[maxn];
int q[2][maxn],ll[2],rr[2];
bool cmp(node a,node b){ return a.x<b.x;}

int check(int w)
{
	ll[0]=ll[1]=rr[0]=rr[1]=1;
	for(int i=1;i<=n;++i)
	{
		while(ll[0]<rr[0]&&p[i].x-p[q[0][ll[0]]].x>w) ++ll[0];
		while(ll[1]<rr[1]&&p[i].x-p[q[1][ll[1]]].x>w) ++ll[1];
		
		while(ll[0]<rr[0]&&p[i].y<=p[q[0][rr[0]-1]].y) --rr[0];
		while(ll[1]<rr[1]&&p[i].y>=p[q[1][rr[1]-1]].y) --rr[1];
		
		q[0][rr[0]++]=i; q[1][rr[1]++]=i;
		
		if(p[q[1][ll[1]]].y-p[q[0][ll[0]]].y>=D) return 1;
	}
	return 0;
}

int main()
{
	n=read();D=read();
	for(int i=1;i<=n;++i)
	p[i].x=read(),p[i].y=read(),mx=max(mx,p[i].x);
	
	sort(p+1,p+1+n,cmp);
	int L=0,R=mx,ans=0;
	while(L<R)
	{
		int mid=L+R>>1;
		if(check(mid)) R=mid,ans=mid;
		else L=mid+1;
	}
	
	if(ans==0) printf("-1");
	else printf("%d",ans);
	return 0;
}


你可能感兴趣的:(单调队列)