【算法学习】单调队列

自闭症网瘾萝莉.ら 提交于 2020-03-18 13:54:47

基础模板

求区间最大值最小值之差在[L,R]范围内的最长区间长度

int l=1;
int l1=1,r1=0;
int l2=1,r2=0;
int ans=0;
for(int r=1;r<=n;r++){
    //删除队尾元素再入队,使满足单调性
    while(l1<=r1 && a[r]>a[mxq[r1]]){
        r1--;
    }
    mxq[++r1]=r;
    while(l2<=r2 && a[r]<a[mnq[r2]]){
        r2--;
    }
    mnq[++r2]=r;
    //队首元素只能用来维护max和min的最大差值
    while(l<=r && l1<=r1 && l2<=r2 && a[mxq[l1]]-a[mnq[l2]]>k){
        l++;
        //删去队头无效元素
        while(l1<=r1 && mxq[l1]<l){
            l1++;
        }
        while(l2<=r2 && mnq[l2]<l){
            l2++;
        }
    }
    //根据最小差值的限制,更新答案
    if(l1<=r1 && l2<=r2 && a[mxq[l1]]-a[mnq[l2]]>=m){
        ans=max(ans,r-l+1);
    }
}

单调队列常用作有大小限制,带删除操作的优先队列?可用来优化dp,或其他类似状态转移。

例题

HDU3530

即上面的模板题

HDU3706

题意

给定\(n\)\(A\)\(B\),生成一个序列\(S\),定义\(T_i\)为以\(i\)结尾的长度为\(A\)的一段区间的\(S_i\)最小值,求所有\(T_i\)的乘积。

分析

读懂题意之后,这个模型就是区间长度固定的最小值,单调队列维护即可。

代码

#include <bits/stdc++.h>
using namespace std;
const int N=1e7+10;
int n,A,B;
int q[N];
int p[N];
int main(void){
    // freopen("in.txt","r",stdin);
    while(~scanf("%d%d%d",&n,&A,&B)){
        int l=1;
        int l1=1,r1=0;
        int l2=1,r2=0;
        int ans=1;
        p[0]=1;
        for(int i=1;i<=n;i++){
            p[i]=1ll*p[i-1]*A%B;
        }
        for(int r=1;r<=n;r++){
            while(l1<=r1 && p[r]<p[q[r1]]){
                r1--;
            }
            q[++r1]=r;
            while(l<=r && l1<=r1 && r-q[l1]>(A+1)){
                l++;
                while(l1<=r1 && q[l1]<l){
                    l1++;
                }
            }
            ans=1ll*ans*p[q[l1]]%B;
        }
        printf("%d\n",ans);
    }
}

POJ2559

题意

\(n\)个高度,求最大面积的矩形。

分析

经典模型之一,正反两个方向用单调队列预处理出每个值作为最小值能延伸到的位置,枚举取面积最大值即可。

代码

#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N=1e5+50;
int n,a[N];
int q[N];
int le[N],ri[N];
int main(void){
    // freopen("in.txt","r",stdin);
    while(~scanf("%d",&n) && n){
        for(int i=1;i<=n;i++){
            scanf("%d",&a[i]);
        }
        int l=1;
        int l1=1,r1=0;
        for(int r=1;r<=n;r++){
            while(l1<=r1 && a[r]<a[q[r1]]){
                ri[q[r1]]=r-1;
                r1--;
            }
            q[++r1]=r;
            l++;
        }
        while(l1<=r1){
            ri[q[r1]]=n;
            r1--;
        }
        l=1;
        l1=1,r1=0;
        for(int r=n;r>=1;r--){
            while(l1<=r1 && a[r]<a[q[r1]]){
                le[q[r1]]=r+1;
                r1--;
            }
            q[++r1]=r;
            l++;
        }
        while(l1<=r1){
            le[q[r1]]=1;
            r1--;
        }
        ll ans=0;
        for(int i=1;i<=n;i++){
            ans=max(ans,1ll*(ri[i]-le[i]+1)*a[i]);
        }
        printf("%lld\n",ans);
    }
    return 0;
}

CF91B

题意

给一个序列,询问每个数和最右边一个比其小的数中间相间数的个数。

分析

维护一个单调递减队列,只允许队尾加入更小的元素,每次二分查找最远的下标

  • 单调队列一个新的用法,每次对一个元素不一定要入队,像这个题,有用的是最远的下标,所以如果这个数不是最小的(不能直接加入队列),那就没有必要加入,因为队列中肯定已经存在一个比其更小且更远的元素。

代码

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+50;
int n,a[N],ans[N];
//维护一个单调递减队列,只允许队尾加入更小的元素,每次二分查找最远的下标
int decQ[N];
int main(void){
    // freopen("in.txt","r",stdin);
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
    }
    int l=n;
    int l1=1,r1=0;
    for(int r=n;r>=1;r--){
        bool ac=false;
        int t=0;
        if(l1>r1 || a[r]<=a[decQ[r1]]){
            decQ[++r1]=r;
            ans[r]=-1;
        }else{
            int ll=l1,rr=r1;
            int t=0;
            while(ll<=rr){
                int mid=(ll+rr)/2;
                if(a[decQ[mid]]<a[r]){
                    t=decQ[mid];
                    rr=mid-1;
                }else{
                    ll=mid+1;
                }
            }
            ans[r]=t-r-1;
        }
        
    }
    for(int i=1;i<=n;i++){
        printf("%d%c",ans[i],i==n?'\n':' ');
    }
    return 0;
}

CF251A

题意

给一个有序序列,求满足最大值减最小值之差小于等于d的三元组个数。

分析

由于序列保证有序,所以只需要用一个普通的序列类似尺取的方法维护当前这个数作为最大值的贡献。

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+50;
int n,d,a[N];
int q[N];
int main(void){
    // freopen("in.txt","r",stdin);
    scanf("%d%d",&n,&d);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
    }
    ll ans=0;
    int l=1,r=0;
    for(int i=1;i<=n;i++){
        //考虑每个数加进去后的贡献
        q[++r]=a[i];
        while(l<=r && a[i]-q[l]>d){
            l++;
        }
        int siz=r-l+1;
        ans+=1ll*(siz-1)*(siz-2)/2;
    }
    printf("%lld\n",ans);
    return 0;
}

HDU3415

题意

求环上的长度不超过\(k\)的最大连续和。

分析

连续子段和考虑前缀和来计算,枚举每个点作为右端点,用单调队列维护最多前\(k\)个数的前缀和最小值。

代码

#include <bits/stdc++.h>
using namespace std;
const int N=2e5+50;
const int INF=0x3f3f3f3f;
int T,n,k,a[N],pre[N],q[N];
int main(void){
    // freopen("in.txt","r",stdin);
    scanf("%d",&T);
    while(T--){
        scanf("%d%d",&n,&k);
        pre[0]=0;
        for(int i=1;i<=n;i++){
            scanf("%d",&a[i]);
            pre[i]=pre[i-1]+a[i];
        }
        for(int i=n+1;i<=n+n;i++){
            pre[i]=pre[i-1]+a[i-n];
            a[i]=a[i-n];
        }
        int l1=1,r1=0;
        int ans=-INF;
        int al=0,ar=0;
        for(int r=1;r<=n+k-1;r++){
            while(l1<=r1 && pre[r-1]<pre[q[r1]]){
                r1--;
            }
            q[++r1]=r-1;
            while(l1<=r1 && r-q[l1]>k){
                l1++;
            }
            if(l1<=r1){
                int tmp=pre[r]-pre[q[l1]];
                if(tmp>ans){
                    ans=tmp;
                    al=q[l1]+1;
                    ar=r;
                }
            }   
        }
        printf("%d %d %d\n",ans,(al-1)%n+1,(ar-1)%n+1);
    }
    return 0;
}

POJ2823

题意

求固定长度\(k\)的区间最大值和最小值。

分析

也是经典模型之一,单调队列维护,去除过老的元素。

代码

#include <cstdio>
#include <algorithm>
#include <cctype>
using namespace std;
const int N=1e6+50;
int n,k,a[N];
int q[N];
int mx[N],mn[N];
int main(void){
    // freopen("in.txt","r",stdin);
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
    }
    int l=1;
    int l1=1,r1=0;
    for(int r=1;r<=n;r++){
        while(l1<=r1 && a[r]>a[q[r1]]){
            r1--;
        }
        q[++r1]=r;
        while(l<=r && l1<=r1 && r-q[l1]+1>k){
            l++;
            while(l1<=r1 && r-q[l1]+1>k){
                l1++;
            }
        }
        mx[r]=a[q[l1]];
    }
    l=1;
    l1=1,r1=0;
    for(int r=1;r<=n;r++){
        while(l1<=r1 && a[r]<a[q[r1]]){
            r1--;
        }
        q[++r1]=r;
        while(l<=r && l1<=r1 && r-q[l1]+1>k){
            l++;
            while(l1<=r1 && r-q[l1]+1>k){
                l1++;
            }
        }
        mn[r]=a[q[l1]];
    }
    for(int i=k;i<=n;i++){
        printf("%d%c",mn[i],i==n?'\n':' ');
    }
    for(int i=k;i<=n;i++){
        printf("%d%c",mx[i],i==n?'\n':' ');
    }
    return 0;
}

19牛客多校第三场F

题意

给一个矩阵,求出最大的子矩阵满足最大值和最小值之差不超过\(m\)

分析

数据范围给的是\(n^3\)的做法,对于二维的一个子矩阵,需要\(n^2\)的时间来枚举上下底,然后使用单调队列维护横向的最大值与最小值之差。

关键一点在于枚举上下底\(i\)行和\(j\)行后,中间的二维矩阵的每一列其实就可以用这一列的最大值和最小值来表示,其他数值都已经无关紧要了,从而转化为一维数组的情况。

代码

#include <bits/stdc++.h>
using namespace std;
const int N=505;
const int INF=0x3f3f3f3f;
int T,n,m,a[N][N];
int mx[N],mn[N];
int mxq[N],mnq[N];
int main(void){
    scanf("%d",&T);
    while(T--){
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
                scanf("%d",&a[i][j]);
            }
        }
        int ans=0;
        //枚举列
        for(int i=1;i<=n;i++){
            for(int j=0;j<=n;j++){
                mx[j]=0;
                mn[j]=INF;
            }
            for(int j=i;j<=n;j++){
                int l=1;
                int l1=1,r1=0;
                int l2=1,r2=0;
                //枚举右边界,维护最小可行左边界
                for(int r=1;r<=n;r++){
                    mx[r]=max(mx[r],a[r][j]);
                    mn[r]=min(mn[r],a[r][j]);
                    //维护两个单调队列mxq(最大值递减),mnq(最小值递增)
                    while(l1<=r1 && mx[r]>mx[mxq[r1]]){
                        r1--;
                    }
                    mxq[++r1]=r;
                    while(l2<=r2 && mn[r]<mn[mnq[r2]]){
                        r2--;
                    }
                    mnq[++r2]=r;
                    //维护两个单调队列使得max-min<=m
                    while(l<=r && l1<=r1 && l2<=r2 && mx[mxq[l1]]-mn[mnq[l2]]>m){
                        l++;
                        //删去无效元素
                        while(l1<=r1 && mxq[l1]<l){
                            l1++;
                        }
                        while(l2<=r2 && mnq[l2]<l){
                            l2++;
                        }
                    }
                    ans=max(ans,(j-i+1)*(r-l+1));
                }
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!