[整理] 单调队列题目整理

写在前面

连着刷了 4 天 12 道单调队列,整理一下这些题的解法,找一找关于单调队列共性的东西。

Luogu 1725 琪露诺

 dp 方程想起来比较简单,但是显然暴力转移会 T,考虑单调队列。

因为题目是要用 dp[i-k] 来更新 dp[i] ,所以单调队列在转移的时候 push 的应该是 dp[i-l],而不是 dp[i]

也就是说,对于这种从前面来更新当前的转移, push 的时候应该 push i-k (k 为一个常数) 的值,push 完之后再用队头更新当前的答案。

#include<cstdio>
#include<cstring>
#include<iostream>
#define N 200005

int dp[N];
int n,l,r;
int val[N];
int q[N],hd,tail;

signed main(){
    scanf("%d%d%d",&n,&l,&r);
    if(l>r) l^=r^=l^=r;
    for(int i=0;i<=n;i++) scanf("%d",&val[i]);
    hd=tail=1;
    q[1]=0;
    for(int i=l;i<=n;i++){
        while(hd<=tail&&i-q[hd]>r) hd++;
    //    printf("i=%d,q[hd]=%d\n",i,q[hd]);
        while(hd<=tail&&dp[q[tail]]<=dp[i-l]) tail--;
        q[++tail]=i-l;
        dp[i]=dp[q[hd]]+val[i];
    //    printf("dp[i]=%d\n",dp[i]);
    }
    int maxn=0;
    for(int i=n-r+1;i<=n;i++)
        maxn=std::max(maxn,dp[i]);
    printf("%d\n",maxn);
    return 0;
} 
View Code

Luogu 3088 挤奶牛

显然要正反用两遍单调队列。

但是一开始的想法跟单调栈有点类似,就是在 push 元素的时候,如果要 push 的元素是队尾元素高度的两倍,那么就给队尾的元素打一个标记,然后 tail--

但是这样的做法会造成漏判,所以,我们要换一种判断的方法。

其实也很简单,就是把判断从当前元素判断队中的元素变为队中的元素判断当前元素。

在循环中,合法性判断完之后,如果队头的元素是当前元素高度的两倍,那么把当前元素打一个标记,这样就完美解决了漏判的问题。

#include<cstdio>
#include<algorithm>
#define N 50005

int n,m;
int l[N],r[N];
int q[N],hd,tail;

struct Node{
    int x,height;
}node[N];

bool cmp(Node a,Node b){
    return a.x<b.x;
}

signed main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d%d",&node[i].x,&node[i].height);
    std::sort(node+1,node+1+n,cmp);
    hd=1;tail=0;
    for(int i=1;i<=n;i++){
        while(hd<=tail&&node[i].height>=node[q[tail]].height) tail--;
        q[++tail]=i;
        while(hd<=tail&&node[i].x-node[q[hd]].x>m) hd++;
        if(node[q[hd]].height>=2*node[i].height) l[i]=1;
    }
    hd=1,tail=0;
    for(int i=n;i;i--){
        while(hd<=tail&&node[i].height>=node[q[tail]].height) tail--;
        q[++tail]=i;
        while(hd<=tail&&node[q[hd]].x-node[i].x>m) hd++;
        if(node[q[hd]].height>=2*node[i].height) r[i]=1;
    }
    int ans=0;
    for(int i=1;i<=n;i++) if(l[i]&&r[i]) ans++;
    printf("%d",ans);
    return 0;
}
View Code

Luogu 1714 切蛋糕

想做法还是花了一些时间的,但是想到后秒出代码。

对于这个序列,我们求一遍前缀和,记录数组为 qzh。

定义 f[i] 表示吃第 i 块蛋糕以及 i 之前连续一段长度小于 m 的蛋糕的幸运值。

显然 f[i]=max{qzh[i]-qzh[i-k]},1≤k≤m

那么我们可以维护一个前缀和单调上升的队列啊,队尾即为最小值

水题水题~

#include<cstdio>
#include<iostream>
#define N 500005

int n,m;
int maxn;
int qzh[N];
int q[N],hd,tail;

signed main(){
    scanf("%d%d",&n,&m);
    for(int x,i=1;i<=n;i++) scanf("%d",&x),qzh[i]=qzh[i-1]+x;
    hd=1,tail=0;
    for(int i=1;i<=n;i++){
        while(hd<=tail&&i-q[hd]>m) hd++;
        maxn=std::max(maxn,qzh[i]-qzh[q[hd]]);
        while(hd<=tail&&qzh[q[tail]]>=qzh[i]) tail--;
        q[++tail]=i;
    }
    printf("%d\n",maxn);
    return 0;
}
View Code

Luogu 2629 好消息,坏消息

这题就稍微有些难了,主要是拆环成链没有想到。

如果想到了这一步,那么还是比较简单的。

还是先求出前缀和数组 qzh。

我们假设 k 满足条件,那么对于 min{qzh[i]},i∈[k,k+n-1],一定有 qzh[i]-qzh[k-1]>0。

有了这个性质,我们维护一个单调上升的队列就好了,队头即为当前前缀和的最小值。

#include<cstdio>
#include<iostream>
#define N 2000005

int n,ans;
int q[N],hd,tail;
int val[N],qzh[N];

signed main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&val[i]),val[i+n]=val[i];
    for(int i=1;i<=(n<<1);i++) qzh[i]=qzh[i-1]+val[i];
    hd=0,tail=1;
    for(int i=1;i<=n;i++){
        while(hd<=tail&&qzh[i]<qzh[q[tail]]) tail--;
        q[++tail]=i;
    }
    for(int k=1;k<=n;k++){
        while(hd<=tail&&q[hd]<k) hd++;
        if(hd<=tail&&qzh[q[hd]]-qzh[k-1]>=0) ans++;
        while(hd<=tail&&qzh[q[tail]]>=qzh[k+n]) tail--;
        q[++tail]=k+n;
    }
    printf("%d\n",ans);
    return 0;
}
View Code

五  Luogu 2422 良好的感觉

这也是想了半天啊。。。

要求一段区间内最小值乘上区间和最大

对于元素 i ,可以单调栈找出它左边第一个比它小的元素的位置 l 和右边第一个比它小的元素的位置 r。

那么我们形象地说,当前这个元素 i 统治了 [l+1,r-1] 这个区间 (l+1 是有可能等于 r-1 的),那么对于 [l+1,r-1] 这段区间贡献的答案,显然就是 val[i]*(qzh[r-1]-qzh[l])。

答案取个 max 就好了。

注意开 long long !

#include<cstdio>
#include<iostream>
#define N 100005
#define int long long

int n;
int val[N];
int qzh[N];
int l[N],r[N];
int stk[N],top;

signed main(){
    scanf("%lld",&n);
    for(int i=1;i<=n;i++) scanf("%lld",&val[i]),qzh[i]=qzh[i-1]+val[i];
    for(int i=1;i<=n;i++){
        while(top&&val[i]<val[stk[top]]) l[stk[top]]=i,top--;
        stk[++top]=i;
    }
    while(top) l[stk[top--]]=n+1;
    for(int i=n;i;i--){
        while(top&&val[i]<val[stk[top]]) r[stk[top]]=i,top--;
        stk[++top]=i;
    }
    while(top) r[stk[top--]]=0;
    int ans=-1234567890;
    for(int i=1;i<=n;i++)
        ans=std::max(ans,(qzh[l[i]-1]-qzh[r[i]])*val[i]);
    printf("%lld\n",ans);
    return 0;
}
View Code

SCOI2009 生日礼物

大意是求最短的一段区间,使其包含所有 m 种元素 ( m≤60 )

最开始先扫一遍,不动头指针,只动尾指针,直到使 [1,tail] 这段区间里包含所有的种类。

然后开始尝试动头指针,直到不能动为止,那么当前的 [head,tail] 里包含了所有的种类。

接着从 tail+1 开始循环,如果能让头指针往右动,就尽可能让它动,再将当前元素扔进队列里,用 tail-head 更新答案就好。

这种求区间包含所有种类元素的方法还是要学一学的。

#include<cstdio>
#include<iostream>
#include<algorithm>
#define N 1000005
#define int long long

int n,m,ans;
int cnt,tot;
int exis[65];
int q[N],l=1,r;

struct Node{
    int x,idx;
    friend bool operator<(Node a,Node b){
        return a.x<b.x;
    }
}node[N];

void update(int x){
    if(!exis[node[x].idx]) tot++;
    exis[node[x].idx]++;
}

bool check(int x){
    if(exis[node[x].idx]>1) return 1;
    return 0;
}

void down(int x){
    exis[node[x].idx]--;
}

signed main(){
    scanf("%lld%lld",&n,&m);
    for(int T,i=1;i<=m;i++){
        scanf("%lld",&T);
        for(int x,j=1;j<=T;j++){
            scanf("%lld",&x);
            node[++cnt].x=x;
            node[cnt].idx=i;
        }
    }
    std::sort(node+1,node+1+cnt);
    while(tot<m&&r<cnt) q[++r]=r,update(r);
    while(l<=r&&check(l)) down(l),l++;
    ans=node[r].x-node[l].x;
    for(int i=r+1;i<=n;i++){
        q[++r]=i;
        exis[node[i].idx]++;
        while(l<=r&&check(l)) down(l),l++;
        ans=std::min(ans,node[r].x-node[l].x);
    }
    printf("%lld\n",ans);
    return 0;
}
View Code

Luogu 2698 花盆

大意是求一段最小的区间,使得落在这段区间上的水滴的最大时间差满足要求

注意到答案是满足单调性的,所以二分答案后处理

但是由于我太蠢,没有想到单调队列的解题方法,于是敲了一发 ST 表。。。

单调队列其实就是两个滑动窗口维护区间最大和最小就行了,为什么没想到啊。。。

代码只有 ST 表版本的就不贴了

POI2011 Temperature

大意是每个元素都有一个浮动区间,求最长连续的一段,满足该段内元素可能单调不降。

最开始想着扫一遍出结果的想法是错的就不说了。。

正解依然是单调队列(废话不然你写进来干嘛)

观察到满足题意的一段一定满足这样一个性质,即一段中最大的左端点一定小于等于最后一个元素的右端点。

那么我们维护一个左端点单调不降的队列就好了

这样队头存的就是这一段区间中最大的左端点

如果队头比当前元素的右端点大,那么就 head++,直到满足题意

然后用当前元素位置减去队头元素位置更新即可

往队尾 push 元素是一样的,如果当前的左端点大于队尾的左端点,tail-- 就好了,因为假如有一个数列满足当前元素,那么也一定满足左端点更小的元素。

#include<cstdio>
#include<iostream>
#define N 1000005

int n,ans;
int l[N],r[N];
int q[N],hd=1,tail;

signed main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d%d",&l[i],&r[i]);
    for(int i=1;i<=n;i++){
        while(hd<=tail&&r[i]<l[q[hd]]) /*printf("i=%d,q[hd]=%d\n",i,q[hd]),*/hd++;
        if(hd<=tail)
            ans=std::max(ans,i-q[hd-1]);
        //printf("q[hd]=%d,i=%d\n",q[hd],i);
        while(hd<=tail&&l[i]>=l[q[tail]]) /*printf("i=%d,q[tail]=%d\n",i,q[tail]),*/tail--;
        q[++tail]=i;
    }
    printf("%d\n",ans);
    return 0;
}
View Code

呼大概就这么多吧~

posted @ 2018-04-15 20:12  YoungNeal  阅读(1713)  评论(3编辑  收藏  举报