• 第九章:单调栈与单调队列


    一、单调栈

    1、什么是单调栈?

    单调栈顾名思义就是栈中的元素是具有单调性的,比如依次增大的数则具有单调递增的性质,依次减小的数则具备单调递减的性质。

    2、单调栈的模板

    (1)问题:

    在这里插入图片描述

    (2)分析:

    我们可以写出一个暴力做法:如下:

    #include
    using namespace std;
    const int N=100010;
    int a[N];
    int main()
    {
        int n;
        cin>>n;
        for(int i=0;i<n;i++)
        {
            scanf("%d",a+i);
        }
        for(int i=0;i<n;i++)
        {
            int j,flag=0;
            for(j=i-1;j>=0;j--)
            {
                if(a[j]<a[i])
                {
                    flag=1;
                    break;
                }
            }
            if(flag)cout<<a[j]<<" ";
            else cout<<-1<<" ";
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    但是这种暴力做法的时间复杂度是O(N2),因此当我们提交的时候会发生超时的情况。因此,我们需要寻找另外的算法来优化时间复杂度。

    此时,我们可以采用空间换时间的思想,具体思路如下:

    我们除了创建一个数组去存储输入的数据外,我们再开辟一块空间来处理数据。题目中提到的是左端的第一个,此时我们很容易就联想到了栈这种数据结构,因为这种数据结构是先进后出,假设我们再存储到数组中的同时,也存储到栈中,那么我们的栈顶元素一定是前距离a[i]左端最近的数字。

    因此,当我们使用栈的时候,就能够保证我们输出的栈顶元素是距离目标元素最近的,那么我们接下来的问题就是如何保证,我们输出的栈顶元素一定是比目标元素小呢?

    我们看下面的思路:

    • step1 : 在栈不为空的条件下,只要栈顶元素大于等于栈顶元素,那么我们就删除掉栈顶元素。
    • step2 : 让目标元素进栈。

    然后我们来分析一下上面的思路:
    步骤一就保证了栈中的任意一个元素,一定是小于后一个元素的。所以此时栈中的元素呈现了单调递增的特性。

    接着我相信大家会在两个点上感到疑惑:

    1、删除的元素会不会是后续过程中的答案?

    我们看下面的这种情况下,5是否有可能成为后续过程的答案?
    假设我们的目标元素是6,那么5小于6,但是由于5>3,根据不等式的传递性,3也小于6,但是3距离元素6更近,所以3一定是答案。那么这是数字3存在的情况。我们假设数字3被删除了,我们假设3遇到了2,此时3大于等于2,所以3会被删除,但是此时入栈的元素是不是2,很明显再2的存在下,5更不可能成为答案。综上所述,我们删除的元素是不可能成为答案的。
    在这里插入图片描述
    2、目标元素一定要入栈吗?

    答案是一定的,因为通过步骤一的循环处理,栈具备单调递减的特点,因此我们的目标元素就会成为栈中最小的元素,同时这个元素还是距离下一个元素最近的。假设这个元素都大于目标元素,那么剩余元素必定大于目标元素,所以这个栈中就不会再存在答案了。综上所述,我们的目标元素是最有可能成为下一次判断时的答案。

    因此,我们的思路一定能够输出正确的答案,那么我们如何实现呢?

    #include
    using namespace std;
    const int N=100010;
    int a[N],stk[N],top;
    int main()
    {
        int n;
        scanf("%d",&n);
        for(int i=0;i<n;i++)
        {
            scanf("%d",a+i);
            while(stk&&stk[top]>=a[i])top--;
            if(!top)cout<<-1<<" ";
            else cout<<stk[top]<<" ";
            stk[++top]=a[i];
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    我们发现上面的时间复杂度是O(N),大大的优化了时间。

    二、单调队列

    1、什么是单调队列

    单调队列就是队列中的元素具有单调性的队列。

    2、单调队列模板

    (1)问题

    在这里插入图片描述

    (2)分析

    我们以最小值为例:

    这道题我们同样可以采用暴力的做法:两个for循环相互嵌套。我们发现这种情况的时间复杂度是O(N2)。很明显,这种算法是非常低效的。

    那么我们换一种算法:

    我们先来思考一下,这道题适合什么样的数据结构?不难发现,这个滑动窗口是从前向后移动,即先进先出。所以我们想到了队列这种数据结构来存储。

    那么队列是在队头输出的,因此我们需要保证队头输出的数据是最小值。此时受到刚才单调栈的影响,我们很容易想到这里应该是构造一个单调递增的队列,这样我们就能够保证队列中的第一个元素是最小的。

    此时大家就会有一个疑问,为什么必须单调呢?我们明明只需要保证第一个最小即可。那么我们假设第一个是最小的,第二个是最大的。当我们的第一个元素滑出窗口的时候,第二个元素成为了第一个元素,此时第一个元素就不再是最小的。因此,我们保证单调性的时候,就能够保证每个元素做队头的时候,都小于后续的元素。

    我们如何实现呢?单调递增的特点就是前一个元素小于该元素。接着我们就能写出下列的步骤:

    • step1 : 在队列不为空的条件下,判断第一个元素是否在窗口内。
    • step2 : 若队尾元素大于等于目标元素则删除
    • step3 :目标元素进队。

    在进行步骤1的判断的时候,我们需要一个计数器去记录,但是这样是相当麻烦的,因此我们将下标存储到队列中,这样的话,我们就能够通过下标的方式去判断队头是否出队。

    我们还是解决一下几个问题:
    为什么要不满足单调性的元素可以删除?
    在这里插入图片描述

    在这个队列中,只要有3的存在,4必不可能是答案,那么当3不存在的时候呢?那么4更不可能是答案了,因为我们是在队头删除元素的。所以3出队的前提是4出队。因此只有4没有3的情况是不存在的。所以我们可以删除。

    那么为什么目标元素一定要进队呢?
    因为经过单调性的处理,目标元素就成了队中最大的,虽然目标元素队前的元素比其要小,但是他们都是先出队的,所以当前面的元素都出队的时候,该目标元素有可能成为答案,所以我们要让每个目标元素的下标都进队。

    #include
    using namespace std;
    const int N=1000010;
    int a[N],q[N],h,t;
    int main()
    {
        int n,k;
        cin>>n>>k;
        for(int i=0;i<n;i++)
        {
            scanf("%d",a+i);
            if(h<=t&&q[h]<i-k+1)h++;
            while(h<=t&&a[q[t]]>=a[i])t--;
            q[++t]=i;
            if(i-k+1>=0)
            printf("%d ",a[q[h]]);
        }
        puts("");
        h=0,t=-1;
        for(int i=0;i<n;i++)
        {
            if(h<=t&&q[h]<i-k+1)h++;
            while(h<=t&&a[q[t]]<=a[i])t--;
            q[++t]=i;
            if(i-k+1>=0)
            printf("%d ",a[q[h]]);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    我们发现,判断单调性的时候,我们是在队尾删除,判断是否在窗口内的时候,是在队头删除的。所以这不是普通的队列,而是双端队列,即STL中的deque。所以我们可以通过deque容器来实现。

    #include
    #include
    #include
    using namespace std;
    int main()
    {
        int n,k,b;
        cin>>n>>k;
        vector<int>a;
        deque<int>q;
        for(int i=0;i<n;i++)
        {
            scanf("%d",&b);
            a.push_back(b);
            if(!q.empty()&&q.front()<i-k+1)q.pop_front();
            while(!q.empty()&&a[q.back()]>=a[i])q.pop_back();
            q.push_back(i);
            if(i-k+1>=0)printf("%d ",a[q.front()]);
        }
        puts("");
        q.clear();
        for(int i=0;i<n;i++)
        {
            if(!q.empty()&&q.front()<i-k+1)q.pop_front();
            while(!q.empty()&&a[q.back()]<=a[i])q.pop_back();
            q.push_back(i);
            if(i-k+1>=0)printf("%d ",a[q.front()]);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
  • 相关阅读:
    Web框架介绍
    【月度总结】数据库&Python&Excel_202206
    webservice初探
    4.2 metasploit 开发 exploit
    在PyCharm快速加载第三方模块的设置方法
    ffplay源码分析:图像格式转换
    Mybatis基础操作
    CentOS7安装Oracle数据库的全流程
    Kernel源码笔记之文件系统:2. fuse——挂载
    修改 Git 已经提交记录的 用户名 和邮箱
  • 原文地址:https://blog.csdn.net/weixin_72060925/article/details/127952750