单调栈及模板

单调栈及模板

1.单调栈的应用

    单调栈的应用是非常有限的,一般只适用于如下的模型:
    给定一个序列,对于序列中的每一个数,找到这个数左边/右边离它最近的比它大/小的数是谁?如果找不到,返回-1或其他操作。
    如果某一个问题,满足上述的模型,那么我们就可以用单调栈来进行解决。

img

    例如,我们可以通过如上的案例,来求得:一个序列(3 4 2 7 5 )中,对于每一个数,左边离它最近的且比它小的数是谁,如果不存在,则返回-1。

2. 暴力解法分析

img

    对于上述问题,我们可以采用如上的暴力算法来进行求解。
    但是,对于上述的做法,时间复杂度很高,是O(n²)。因此,我们可以使用单调栈来进行优化。

3. 单调栈的过程

    对于上述的暴力解法,我们可以运用一个栈来进行等价化。
    具体来说,每当i往后进行移动时,i之前的元素都会存入到栈中。例如,当遍历到元素ai时,栈中会存放a1,a2,...,ai-1(其中a1在栈底,ai-1在栈顶)。当寻找离ai最近的且比它小的元素时,从栈顶到栈底遍历,找到第一个满足条件的元素即为答案。
    但是,对于上述的做法,时间复杂度仍是没有改变。那么我们如何从上述的做法中提取出一些性质,来达到优化的目的呢?
    根据上述的做法,我们可以发现如下性质:
    1. 对于栈中的元素,如果存在两个元素ax和ay, 满足:ax >= ay 且 x < y。(实际上,不严格来说,ax和ay是一对逆序)那么,对于上述的条件(离ai最近的且比它小的元素)话,ax永远不会作为答案输出。(因为,ay比ax更好)
    2. 因此,对于上述的性质,我们可以将栈中所有类似于ax这样的元素删掉。删掉之后,我们不难发现,栈中的元素均满足严格单调递增这样的性质。这也是单调栈这个数据结构的由来。
    3. 根据上述的结论,我们可以得出单调栈的过程如下:
        首先,我们以ai为基准,要寻找离ai左边最近的且比它小的元素。
        1.  我们从栈顶开始遍历,如果栈顶元素比它小,由于栈中均是满足单调递增这个性质,因此栈顶及之后的元素均比它小,因此栈顶就是答案,且把ai放入到栈顶中。
        2.如果栈顶元素比它大,那么栈顶元素永远不会作为答案进行输出。(为什么?假设,我们要考察ai+1这个元素,那么如果栈顶元素和ai均比ai+1要小,且栈顶元素大于等于ai,那么答案一定是ai而不是栈顶元素。所以,根据上述情况栈顶元素永远都不会作为答案输出。)因此把栈顶元素删掉,继续查看栈顶元素,直到找到第一个比它小的数为止。那么,此时就找到了答案,并将ai放入到栈顶中即可。
        3.对于其他的数也是如此。
    这样的话,我们就利用了单调栈来优化了问题。
    因此,对于这样的问题,我们可以得出一个结论:
        如果,题目要求找到每个数左边离它最近的且比它小的数。
        答案:那么,我们可以从左往右遍历,如果栈顶元素比当前数大或相等,那么删掉栈顶元素即可。
        如果,题目要求找到每个数左边离它最近的且比它大的数。
        答案:那么,我们可以从左往右遍历,如果栈顶元素比当前数小或相等,那么删掉栈顶元素即可。
        如果,题目要求找到每个数右边离它最近的且比它小的数。
        答案:那么,我们可以从右往左遍历,如果栈顶元素比当前数大或相等,那么删掉栈顶元素即可。
        如果,题目要求找到每个数右边离它最近的且比它大的数。
        答案:那么,我们可以从右往左遍历,如果栈顶元素比当前数小或相等,那么删掉栈顶元素即可。
    我们可以分析一下,经过单调栈优化的时间复杂度。我们可以发现,数组中的所有元素最多只会进栈一次,出栈一次。因此,加起来最多只会有2n次操作。因此,时间复杂度就是O(n)。

4. 单调栈模板

// 常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
    while (tt && check(stk[tt], i)) tt -- ;
    stk[ ++ tt] = i;
}

5. 例题

https://www.acwing.com/problem/content/832/
#include <iostream>
#include <cstdio>

using namespace std;

int stack[100010];
int tt = 0;

int main(){
    int n;
    scanf("%d",&n);
    for(int i=0;i<n;i++){
        int x;
        scanf("%d",&x);
        //如果栈不为空且栈顶元素大于等于当前元素
        while(tt && stack[tt] >= x){
            //那么将栈顶元素出栈
            tt--;
        }
        //如果遍历之后,栈不为空,那么栈顶元素就是答案。
        //如果遍历之后,栈为空,那么对于当前元素来讲,没有答案,输出-1即可。
        //完成上述操作之后,将当前元素置于栈顶即可。
        if(tt){
            printf("%d ",stack[tt]);
        }else{
            printf("-1 ");
        }
        stack[++tt] = x; 
    }
    return 0;
}
    作者:gao79138
    链接:https://www.acwing.com/
    来源:本博客中的截图、代码模板及题目地址均来自于Acwing。其余内容均为作者原创。
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted @   夏目^_^  阅读(30)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示