单调队列—学习笔记

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

A:
对于这题
首先不难想到先求出数列前缀和sum[]
那么显然问题的答案就是 m a x i = m n (   s u m [ i ] m i n j = i m i 1 ( s u m [ j ] )   )

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

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

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

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

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


完整体面及提交地址
洛谷P1714 切蛋糕

#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<algorithm>
#include<cstring>
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;
}

猜你喜欢

转载自blog.csdn.net/niiick/article/details/80807231