单调队列-原理详解(deque实现)[通俗易懂]

单调队列-原理详解(deque实现)[通俗易懂]一、单调队列的概念:单调队列,即单调递减或单调递增的队列。二、单调队列的性质:1.队列中的元素在原来的列表中的位置是由前往后的(随着循环顺序入队)。2.队列中元素的大小是单调递增或递减的。三、单调队列的特点:从队尾入列,队首或队尾出列。四、例题分析:那么单调队列用什么用呢?单调队列一般用于求区间内的最值问题。看几道题,理解上述内容:1.洛谷P1886…

大家好,又见面了,我是你们的朋友全栈君。

一、单调队列的概念:

单调队列,即单调递减或单调递增的队列。

二、单调队列的性质:

1.  队列中的元素在原来的列表中的位置是由前往后的(随着循环顺序入队)。

2.  队列中元素的大小是单调递增或递减的。

三、单调队列的特点:

从队尾入列,队首或队尾出列。

四、例题分析:

那么单调队列用什么用呢?单调队列一般用于求区间内的最值问题。看几道题,理解上述内容:

1. 洛谷P1886 滑动窗口:https://www.luogu.org/problemnew/show/P1886

看上去不是很难,两个for循环就搞定了,但要知道这样的算法时间复杂度为o(n^2),按题目所给的数据肯定会超时。我们要的是时间复杂度为o(n)的算法,看代码:

#include <bits/stdc++.h>

using namespace std;

const int N=1e6+5;

int m[N];           //用于储存最大值序列

struct node{        //队列的节点,包含元素在列表中原来的位置和值
    int order;
    int value;
}tmp;

deque<node>maxn,minn;    //定义节点类型单调队列,分别记录区域内最大值和最小值

int main()
{
    int n,k,t;           //n,k如题目中含义,t用于暂时储存输入
    int m_lo=0;          //最大值序列下标
    scanf("%d%d",&n,&k);
    for(int i=0;i<n;i++)
    {
        scanf("%d",&t);

        if(!maxn.empty() && i-maxn.front().order>=k) maxn.pop_front();    //单调队列的原理部分
        if(!minn.empty() && i-minn.front().order>=k) minn.pop_front();    //具体看下方说明
        while(!maxn.empty() && maxn.back().value<t) maxn.pop_back();
        while(!minn.empty() && minn.back().value>t) minn.pop_back();

        tmp.value=t;            //节点入列
        tmp.order=i;
        maxn.push_back(tmp);
        minn.push_back(tmp);   

        if(i>=k-1)    //当达到题目要求区间长度时就开始输出最小值序列,并储存最大值序列
        {
            if(i==n-1) printf("%d\n",minn.front().value);
            else printf("%d ",minn.front().value);
            m[m_lo++]=maxn.front().value;
        }

    }

    for(int i=0;i<m_lo;i++)    //输出最大值序列
    {
        if(i==m_lo-1) printf("%d\n",m[i]);
        else printf("%d ",m[i]);
    }
}

说明:这道题,求的是一个区间内的最大值和最小值。对于数组中的某一个元素,我们只要关心它自己和它前k-1个数即可。

那么如何用队列处理呢?先简单说一下解题思路,以求最小值为例:一开始数组下标i为0,我们向队列里不停的放元素,并且一直保持队首位元素为最小值,直到第k个数,此时队首元素即为前k个数的最小值。然后我们把队首输出。继续往下走,走的过程中把队列中下标超出(i-k+1)~i区间范围的数踢掉,继续保持队首元素为区间内最小值,然后输出队首元素。

简单归纳一下,对于每一次循环,我们要做的就是:先踢掉超出区间范围的元素,放入元素并保证队首为数组中当前区间的最小值,输出队首,往复。队列内储存的就是放入元素之前区间内单调递增的最小值序列。

那么我们要解决的问题有两个:

1. 如何去除超出区间范围的元素:这就是代码中node节点的作用了,node节点中储存了原数组的下标,对于每一次新输入的第i个数,因为单调队列内元素位置是从前到后的,我们只要将队首元素的node.order和i进行判断,看此时是否i-node.order>=k即可,是就让队首元素出列。

2. 如何保证队首元素为区间内最小值:对于某一区间,我们每输入一个数,就看看队尾元素的值是否比它大,是就让这个元素出列,直到队列为空或者队尾元素值比这个数小,然后我们把这个数放进去,这个时候队首元素即为区间最小值,输出即可。

合理性分析:从队列为空开始,我们放入第0个数,一直到k-1,每一次输入的时候都让队尾值比当前输入值小的出列,很容易知道此时队首就是[0,k-1]的最小值。那么接下来输入第k个数,可以肯定的是区间[1,k]的最小值与[0,k-1]的最小值有关。有两种情况,一是[1,k]的最小值来自k前面的数,这种情况下,我们看[0,k-1]的情况,若[0,k-1]的最小值来自第0位,就说明队列里肯定有除第0个元素外的元素,即队列里肯定有[1,k-1]的最小值序列,且它们是单调递增的。此时由于第0位超出区间范围,出列,不影响区间[1,k]最小值的查找。若[0,k-1]的最小值来自[1,k-1],自然不影响[1,k]最小值的查找。二是最小值就是k,那么进行操作即可把队列清空,再把它放进即可,此时队首就是k,即最小值。对于一般性的情况,同样有上述关系。

要注意的点:

1. 写if和while的时候一定要先写判断队列是否为空,否则if和while中会进行溢出操作。

2. 判断队首是否出队和队列长度没有确定关系,因为队列不一定都包含了整个区间的元素。

3. 对于重复元素去不去除都可以,去除可以保证队列中至少有一个,不去除不影响队首,且在遇到更小的值时都会出队。

4. 这个算法只有一个for循环,且每个元素都只出入一次,所以时间复杂度为o(n)。 

 

2. P1440 求m区间内的最小值:https://www.luogu.org/problemnew/show/P1440

这道题题目可能说的不是很清楚,看样例:4的前两个数中最小值是1,3的前两个数中最小值也是1。

这题和上一题基本一样,不同的是这回窗口不包括当前元素,即当前元素不参与比较,每一次循环输出上一次的结果即可。

#include <bits/stdc++.h>

using namespace std;
const int N=2e6+5;
int a[N];
struct node{
    int order;
    int value;
}tmp;
deque<node>vis;
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=0;i<n;i++)
    {
        scanf("%d",&a[i]);
        tmp.order=i;
        tmp.value=a[i];
        if(i==0) printf("%d\n",0);
        else printf("%d\n",vis.front().value);
        if(i-vis.front().order>=m) vis.pop_front();
        while(!vis.empty() && vis.back().value>a[i]) vis.pop_back();
        vis.push_back(tmp);
    }
}

3. P2032 扫描:https://www.luogu.org/problemnew/show/P2032

模板题,直接写:

#include <bits/stdc++.h>

using namespace std;
struct node{
    int order;
    int value;
}tmp;
deque<node>vis;
int main()
{
    int n,k,t;
    scanf("%d%d",&n,&k);
    for(int i=0;i<n;i++)
    {
        scanf("%d",&t);
        if(!vis.empty() && i-vis.front().order>=k) vis.pop_front();
        while(!vis.empty() && vis.back().value<t) vis.pop_back();
        tmp.order=i;
        tmp.value=t;
        vis.push_back(tmp);
        if(i>=k-1)
        {
            printf("%d\n",vis.front().value);
        }
    }
}

4. P1714 切蛋糕:https://www.luogu.org/problemnew/show/P1714

这题乍一看好像可以用尺取做,但其实不行,因为左右指针移动后是没有办法回退的。

那么这题怎么用单调队列呢?首先要将问题转化成当前位置所有元素和减去前x个元素和最大值的问题。要得到最大值,只需要前x个元素和最小。那么我们把数组每一位都换成从0~当前位置的和,利用单调队列求当前元素前的元素的最小值,然后用当前元素值减去最小值,每次进行比较即可。

#include <bits/stdc++.h>

using namespace std;

const int N=1e6+5;
int a[N],sum[N];
struct node{
    int order;
    int value;
}tmp;
deque<node>vis;
int main()
{
    int n,m;
    int ans;
    scanf("%d%d",&n,&m);
    for(int i=0;i<n;i++)
    {
        scanf("%d",&a[i]);
        if(i==0) sum[0]=a[i];
        else sum[i]=sum[i-1]+a[i];
        if(!vis.empty() && i-vis.front().order>m) vis.pop_front();     //注意为 > ,自行理解
        while(!vis.empty() && vis.back().value>sum[i]) vis.pop_back();
        tmp.value=sum[i];
        tmp.order=i;
        vis.push_back(tmp);
        if(i==0) ans=sum[i]-vis.front().value;
        else if(i<m) ans=max(ans,sum[i]);
        else ans=max(ans,sum[i]-vis.front().value);
    }
    printf("%d\n",ans);
}

5. P1725 琪露诺:https://www.luogu.org/problemnew/show/P1725

这题估计在出题的时候就有bug,不过不用管,因为数据就是按有bug的方法给的。

首先这是一道dp题,状态转移方程为dp[i]=max(dp[x])+a[i]; 其中(i-r)<=x<=(i-l);

由于要求区间dp[i-r]~dp[i-l]内的最大值,所以用到单调队列。

循环直接从l开始,因为l之前的位置走不到,dp值都为0。

单调队列要维护的是dp的最大值(注意不是a的),且要维护区间的下限与i相差l(这就是定义j的原因),区间固定长度为r-l+1。

#include <bits/stdc++.h>

using namespace std;

const int N=1e6+5;

int a[N],dp[N];

struct node{
    int order;
    int value;
}tmp;

deque<node>vis;

int main()
{
    int n,l,r;
    scanf("%d%d%d",&n,&l,&r);
    for(int i=0;i<=n;i++) scanf("%d",&a[i]);
    for(int i=0;i<l;i++) a[i]=0;        //从0到l-1是走不到的,所以把值改为0
    for(int i=n+1;i<=n+r;i++) a[i]=0;
    int j=0;     //j是要维护的元素下标
    for(int i=l;i<=n+r;i++)
    {
        if(!vis.empty() && j-vis.front().order>=r-l+1) vis.pop_front();
        while(!vis.empty() && vis.back().value<dp[j]) vis.pop_back();
        tmp.order=j;
        tmp.value=dp[j];
        vis.push_back(tmp);
        dp[i]=vis.front().value+a[i];
        j++;
    }
    int ans=-9999999;
    for(int i=n+1;i<=n+r;i++)
    {
        ans=max(ans,dp[i]);
    }
    cout<<ans<<endl;
}

6.  P2629 好消息,坏消息:https://www.luogu.org/problemnew/show/P2629

这题同样要用到元素和,思路是将a复制一遍放到数组末尾,然后再滑动大小为n的窗口,找窗口内的最小值,减去窗口前方值的和,看是否大等于0。

#include <bits/stdc++.h>

using namespace std;

const int N=2e6+5;

int a[N],sum[N];

struct node{
    int order;
    int value;
}tmp;

deque<node>vis;

int main()
{
    int n,k,ans;
    cin>>n;
    for(int i=0;i<n;i++)
    {
        scanf("%d",&a[i]);
        a[i+n]=a[i];
        if(i==0) sum[i]=a[i];
        else sum[i]=sum[i-1]+a[i];
    }
    for(int i=n;i<2*n;i++) sum[i]=sum[i-1]+a[i];
    k=0;        //k用于记录窗口前的位置下标
    ans=0;
    for(int i=0;i<2*n-1;i++)
    {
        if(!vis.empty() && i-vis.front().order>=n) vis.pop_front();
        while(!vis.empty() && vis.back().value>sum[i]) vis.pop_back();
        tmp.order=i;
        tmp.value=sum[i];
        vis.push_back(tmp);
        if(i==n-1 && vis.front().value>=0) ans++;
        else if(i>n-1 && vis.front().value-sum[k]>=0) ans++;
        if(i>n-1) k++;
    }
    cout<<ans<<endl;
}

7.  P2422 良好的感觉:https://www.luogu.org/problemnew/show/P2422

这道题的思路是找到数组a中在以每一个a[i]为最小值的情况下最多能包含多大的区间(因为所有值都大于0,所以在最小值为a[i]的情况下,要尽可能让区间[i,j]范围最大,这样a[i]乘以区间内元素的和就最大),然后比较每一个a[i]与对应区间内元素和的乘积即可,难点在于区间范围的求取。

在这道题中,单调队列的作用是构造一个a的单调递增的序列。对于入队的每一个元素,如果它小于队列的尾元素,说明尾元素的区间下限已经确定,即尾元素本身,而尾元素的区间上限就是队列中尾元素的前一个数,下限的sum值和上限的sum值相减,就得到在以a[i]为最小值的情况下能包含的最大区间的元素和。其它特殊情况参照代码。

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

const int N=3e6+5;

LL a[N],sum[N],multi[N];

struct node{
    LL order;
    LL  value;
}tmp;
deque<node>vis;

int main()
{
    LL n,lo,bef,aft,ans;
    cin>>n;
    for(int i=0;i<n;i++)
    {
        scanf("%lld",&a[i]);
        if(i==0) sum[i]=a[i];
        else sum[i]=sum[i-1]+a[i];
    }
    a[n]=0;        //这个很重要,否则到最后一个数的时候队列没办法清空
    for(int i=0;i<=n;i++)    //注意是<=,原因和上面一样
    {
        while(!vis.empty() && vis.back().value>a[i])
        {
            lo=vis.back().order;
            aft=i-1;
            vis.pop_back();
            if(!vis.empty()) bef=vis.back().order;
            else bef=-1;
            if(bef==-1) multi[lo]=sum[aft];
            else multi[lo]=sum[aft]-sum[bef];
        }
        tmp.order=i;
        tmp.value=a[i];
        vis.push_back(tmp);
    }
    ans=-1;
    for(int i=0;i<n;i++)
    {
        ans=max(ans,multi[i]*a[i]);
    }
    cout<<ans<<endl;
}

 

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/152990.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号