「学习笔记」单调队列

单调栈

      • 一.原理
      • 二.习题练习
        • 1.Sliding Window
        • 2.Non-negative Partial Sums
        • 3.One hundred layer
        • 4.Balanced Playlist
        • 5.OpenStreetMap

一.原理

单调队列,顾名思义,就是从队头到队尾有序的队列,它和单调栈一样也只是一种思想,这种思想借助队列先进先出的特性,在一些特定场合应用起来能够有效的降低时间复杂度,一般用于维护固定合法区间段的最大值或最小值,可用于优化dp(例题三)。实现起来很简单,主要是如何应用这种特性去解决实际问题。下面以几道例题来体会这种思想。

二.习题练习

1.Sliding Window

题目来源:Poj 2823

题意:给定一个序列,固定窗口(区间)大小,求窗口移动过程中的最大值、最小值并输出。

解析:典型的单调队列题目,单调队列就擅长维护固定区间的最大值和最小值,如果要维护最大值,就定义一个从队头到队尾单调递减的队列,这样队头就是这段区间的最大值,维护最小值相反,但每次维护时要判断队头是否在合法区间内,如果不在,要及时让非法队头出队,所以一般要用到双端队列(队头、队尾均可出队进队)且队列存的是下标,我一般用STL的deque,虽然效率上没有手造快,但一般是没问题的,除非被卡常。

#include 
#include 
#include 
#include 
#include 
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
const int maxn=1e6+7;
const int N=6e4+7;
const ll inf=1e12;
#define ft first
#define sd second
#define pb push_back
int t;
int n,k;
deque<int> q;//双端队列存下标
int a[maxn];
int main(){
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
    }
    for(int i=1;i<=n;i++){//维护区间最小值
        while(!q.empty()&&a[q.back()]>a[i]){//不满足递增性质的情况
            q.pop_back();
        }
        q.push_back(i);
        while(!q.empty()&&(q.front()<i-k+1)){//队头非法的情况
            q.pop_front();
        }
        if(i>=k)printf("%d ",a[q.front()]);
    }
    if(k>n)printf("%d",a[q.front()]);//特判如果k比n大
    printf("\n");
    while(!q.empty())q.pop_back();
    for(int i=1;i<=n;i++){//不满足递减性质的情况
        while(!q.empty()&&a[q.back()]<a[i]){
            q.pop_back();
        }
        q.push_back(i);
        while(!q.empty()&&(q.front()<i-k+1)){//队头非法的情况
            q.pop_front();
        }
        if(i>=k)printf("%d ",a[q.front()]);
    }
    if(k>n)printf("%d",a[q.front()]);
    printf("\n");
    return 0;
}
2.Non-negative Partial Sums

题目来源:Hdu 4193

题意:类比第一题,滑动窗口大小为n,共n个这样的序列,让求出有多少个满足任意序列前缀都非负。

解析:先将前缀预处理出来,然后我们用单调队列维护前缀最小值,当区间合法但前缀最小值非负的话,说明这个序列满足题意。

#include 
#include 
#include 
#include 
#include 
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
const int maxn=2e6+7;
const int N=6e4+7;
const ll inf=1e12;
#define ft first
#define sd second
#define pb push_back
int t;
int n,k;
int a[maxn],sum[maxn];
deque<int> q;
int main(){
    while(~scanf("%d",&n)){
        if(!n)break;
        while(!q.empty())q.pop_back();
        for(int i=1;i<=n;i++){
            scanf("%d",&a[i]);
            a[i+n]=a[i];
        }
        for(int i=1;i<=(n<<1);i++){//预处理前缀和
            sum[i]=sum[i-1]+a[i];
        }
        int ans=0;
        for(int i=1;i<n;i++){//前n-1个数先压入队列里
            while(!q.empty()&&sum[q.back()]>sum[i]){
                q.pop_back();
            }
            q.push_back(i);
        }
        for(int i=n;i<(n<<1);i++){//共n个序列,用单调对列维护最小值(递增队列)
            while(!q.empty()&&sum[q.back()]>sum[i]){
                q.pop_back();
            }
            q.push_back(i);
            if(q.front()<=i-n)q.pop_front();
            if(sum[q.front()]-sum[i-n]>=0)ans++;//判断是否合法
        }
        printf("%d\n",ans);
    }
    return 0;
}
3.One hundred layer

题目来源:Hdu 4374

题意: n n n层楼,每层有 m m m部分,起初在1楼第 x x x部分,同层只能向一个方向移动,移动的最大距离不能超过 t t t,所到之处分数尽收囊中。求到顶楼能得到的最大分数。

解析:很容易想到用 d p dp dp求解,我们定义 d p [ i ] [ j ] dp[i][j] dp[i][j]表示到达第 i i i层第 j j j个部分所获得的最大分数,我们先预处理出每一层分数的前缀和,存入 s u m sum sum数组中。
则可得到状态转移方程:

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ k ] + s u m [ i ] [ j ] − s u m [ i ] [ k − 1 ] ) ( j > = k , k > = j − t ) dp[i][j]=max(dp[i-1][k]+sum[i][j]-sum[i][k-1])(j>=k,k>=j-t) dp[i][j]=max(dp[i1][k]+sum[i][j]sum[i][k1])(j>=k,k>=jt)

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ k ] + s u m [ i ] [ k ] − s u m [ i ] [ j − 1 ] ) ( j < k , k < = j + t ) dp[i][j]=max(dp[i-1][k]+sum[i][k]-sum[i][j-1])(jdp[i][j]=max(dp[i1][k]+sum[i][k]sum[i][j1])(j<k,k<=j+t)

初始的时候要将 d p dp dp数组初始化为无穷小。但是很显然,这样会超时,因为我们还要找 s u m [ i ] [ k ] sum[i][k] sum[i][k],我们将式子变一下:

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ k ] − s u m [ i ] [ k − 1 ] ) + s u m [ i ] [ j ] ( j > = k , k > = j − t ) dp[i][j]=max(dp[i-1][k]-sum[i][k-1])+sum[i][j](j>=k,k>=j-t) dp[i][j]=max(dp[i1][k]sum[i][k1])+sum[i][j](j>=k,k>=jt)

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ k ] + s u m [ i ] [ k ] ) − s u m [ i ] [ j − 1 ] ( j < k , k < = j + t ) dp[i][j]=max(dp[i-1][k]+sum[i][k])-sum[i][j-1](jdp[i][j]=max(dp[i1][k]+sum[i][k])sum[i][j1](j<k,k<=j+t)

这样我们发现, m a x max max里面就只与 k k k有关了, s u m [ i ] [ j ] sum[i][j] sum[i][j] s u m [ i ] [ j − 1 ] sum[i][j-1] sum[i][j1]是固定的,对于 m a x max max里面的我们可以考虑用单调队列维护,每一层楼都跑单调队列,对于 p o s ( i , j ) pos(i,j) pos(i,j)维护其左边 t t t d p [ i − 1 ] [ k ] − s u m [ i ] [ k − 1 ] dp[i-1][k]-sum[i][k-1] dp[i1][k]sum[i][k1]的最大值,对于其右边维护 t t t d p [ i − 1 ] [ k ] + s u m [ i ] [ k ] dp[i-1][k]+sum[i][k] dp[i1][k]+sum[i][k]的最大值,这样就能快速求出 d p [ i ] [ j ] dp[i][j] dp[i][j]了,最后再去最后一行找最大值即可。

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
const int maxn=1e4+10;
const int inf=0x3f3f3f3f;
#define pb push_back
#define ft first
#define sd second
#define ms(x,y) memset(x,y,sizeof(x))
int n,m,x,t;
int a[105][maxn];
int sum[105][maxn];
int dp[105][maxn];
int ml[maxn],mr[maxn];//左边最大值,右边最大值
deque<int> q;
int judge1(int x,int y){//左
    return dp[x-1][y]-sum[x][y-1];
}
int judge2(int x,int y){//右
    return dp[x-1][y]+sum[x][y];
}
int main()
{
    while(~scanf("%d%d%d%d",&n,&m,&x,&t)){
        while(!q.empty())q.pop_back();
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                scanf("%d",&a[i][j]);
                sum[i][j]=sum[i][j-1]+a[i][j];//前缀
                dp[i][j]=-inf;
            }
        }
        dp[1][x]=a[1][x];//第一行预处理
        int l=max(1,x-t),r=min(m,x+t);
        for(int i=x;i>=l;i--)dp[1][i]=sum[1][x]-sum[1][i-1];
        for(int i=x+1;i<=r;i++)dp[1][i]=sum[1][i]-sum[1][x-1];
        for(int i=2;i<=n;i++){
            while(!q.empty())q.pop_back();
            for(int j=1;j<=m;j++){//左边最大值
                while(!q.empty()&&judge1(i,q.back())<judge1(i,j)){
                    q.pop_back();
                }
                q.push_back(j);
                while(q.front()<j-t)q.pop_front();
                ml[j]=judge1(i,q.front());
            }
            while(!q.empty())q.pop_back();
            for(int j=m;j>=1;j--){//右边最大值,倒序维护
                while(!q.empty()&&judge2(i,q.back())<judge2(i,j)){
                    q.pop_back();
                }
                q.push_back(j);
                while(q.front()>j+t)q.pop_front();
                mr[j]=judge2(i,q.front());
            }
            for(int j=1;j<=m;j++){//取两者最大
                dp[i][j]=max(ml[j]+sum[i][j],mr[j]-sum[i][j-1]);
            }
        }
        int ans=-inf;
        for(int i=1;i<=m;i++){//去最后一行找最大值
            ans=max(ans,dp[n][i]);
        }
        cout<<ans<<'\n';
    }
    return 0;
}
4.Balanced Playlist

题目来源:codeforces 1237D

题意:一个音乐播放序列,可以循环,每个对于的是音乐的冷门值,求从任意一首音乐开始,到停止的长度。停止表示当前歌曲冷门值小于所听音乐序列段的冷门最大值的一半。

解析:这道题解法很多,可以应用单调队列求解:用一个单调递减队列,维护区间最大值,首先音乐可以循环,所以序列要拓展两倍,以供模拟其循环播放。

首先,播放不会停止的情况是,序列最小值的二倍不小于最大值。特判处理。

然后跑单调队列维护最大值,每次将当前值的二倍与队头对应值进行比较,如果当前值的二倍小于队头最大值的话,队头对应序号的歌曲的结果就确定了。

由于中间为了满足单调性质,部分被弹出了,导致对应歌曲没有更新结果,如果将答案存入 a n s ans ans数组,我们会发现,中间没有被更新的和紧挨着它的后面歌曲的结果有关,刚好为 a n s [ i + 1 ] + 1 ans[i+1]+1 ans[i+1]+1

不妨看这个序列:“1,2,3,4,5,6”,我们发现,由于队列单调的性质,只有 a n s [ 6 ] = 1 ans[6]=1 ans[6]=1,其他均被弹出了,没有更新结果,但其实这些都是和 6 6 6停的地方相同,所以满足前者是后者的结果加一。

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
const int maxn=3e5+10;
const int inf=0x3f3f3f3f;
#define pb push_back
#define ft first
#define sd second
#define ms(x,y) memset(x,y,sizeof(x))
int n,m;
int a[maxn];
int ans[maxn];
deque<int> q;
int main()
{
    while(!q.empty())q.pop_back();
    scanf("%d",&n);
    int mi=inf,ma=-1;
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        a[i+n]=a[i+2*n]=a[i];
        ma=max(ma,a[i]);
        mi=min(mi,a[i]);
    }
    if(mi*2>=ma){//特判
        for(int i=1;i<=n;i++){
            printf("-1%c",i==n?'\n':' ');
        }
        return 0;
    }
    m=n*3;
    for(int i=1;i<=m;i++){//单调递减队列维护最大值
        while(!q.empty()&&a[q.back()]<a[i]){
            q.pop_back();
        }
        q.push_back(i);
        while(!q.empty()&&a[q.front()]>a[i]*2){
            ans[q.front()]=i-q.front();
            q.pop_front();
        }
    }
    for(int i=m;i>=1;i--){//将没更新到的都更新了
        if(!ans[i])ans[i]=ans[i+1]+1;
    }
    for(int i=1;i<=n;i++){
        printf("%d%c",ans[i],i==n?'\n':' ');
    }
    return 0;
}
5.OpenStreetMap

题目来源:codeforces 1195E

题意:给你一直递推关系,让你构造一个 n × m n×m n×m的矩阵,然后让你求所有 a × b a×b a×b的子矩阵的最小值,将他们累加输出。

解析:要求最小值,实际上可以对每一行跑单调队列来维护固定区间 b b b的最小值,可以用单调递增队列来维护最小值,然后存入辅助数组 a n s [ i ] [ j ] ans[i][j] ans[i][j],然后再对 a n s ans ans数组的每一列跑单调队列,维护固定区间 a a a的最小值,最后把合法区间的 a n s ans ans值累加即可。(建议先把前几题做了,这题就好理解了)
以样例为例,我们看下过程:

行\列 1 2 3 4
1 1 5 13 29
2 2 7 17 37
3 18 39 22 47

然后对每一行跑单调队列:

行\列 1 2 3 4
1 1 5 13 29
2 2 7 17 37
3 18 39 22 47

然后对每一列跑单调队列:

行\列 1 2 3 4
1 1 5 13 29
2 1 5 13 29
3 2 7 17 37

其中合法区间段为:
∑ a n s [ i ] [ j ] ( a < = i < = n 且 b < = j < = m ) \sum ans[i][j](a<=i<=n且b<=j<=m) ans[i][j](a<=i<=nb<=j<=m)

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
const int maxn=3e3+10;
const int inf=0x3f3f3f3f;
#define pb push_back
#define ft first
#define sd second
#define ms(x,y) memset(x,y,sizeof(x))
int n,m,a,b;
ll g[maxn][maxn];
ll ans[maxn][maxn];
ll x,y,z;
deque<int> q;
int main()
{
    scanf("%d%d%d%d",&n,&m,&a,&b);
    scanf("%lld%lld%lld%lld",&g[1][1],&x,&y,&z);
    for(int i=1;i<=n;i++){//根据递推关系构造矩阵
        if(i!=1){
            g[i][1]=(g[i-1][m]*x+y)%z;
        }
        for(int j=2;j<=m;j++){
            g[i][j]=(g[i][j-1]*x+y)%z;
        }
    }
    for(int i=1;i<=n;i++){//先对每行跑单调队列
        while(!q.empty())q.pop_back();
        for(int j=1;j<=m;j++){
            while(!q.empty()&&g[i][q.back()]>g[i][j]){
                q.pop_back();
            }
            q.push_back(j);
            while(q.front()<j-b+1)q.pop_front();
            ans[i][j]=g[i][q.front()];
        }
    }
    ll sum=0;
    for(int i=b;i<=m;i++){//对每列跑单调队列
        while(!q.empty())q.pop_back();
        for(int j=1;j<=n;j++){
            while(!q.empty()&&ans[q.back()][i]>ans[j][i]){
                q.pop_back();
            }
            q.push_back(j);
            while(q.front()<j-a+1)q.pop_front();
            if(j>=a)sum+=ans[q.front()][i];//在合法区间内就累加入答案中
        }
    }
    cout<<sum<<'\n';
    return 0;
}

2019.1.19 2019.1.19 2019.1.19

你可能感兴趣的:(题解,数据结构,算法,数据结构,队列)