单调栈讲解及相关题目

单调栈是一种特殊的数据结构,常用于处理有序问题,分为单调递增和单调递减栈。在算法中,单调栈能高效解决寻找序列中某些特定元素的问题,例如寻找最近的比目标值大的或小的元素。通过不断调整栈内元素,可以减少遍历次数,提高时间复杂度。文中通过代码示例展示了单调栈在找左右边界、直方图最大矩形面积等问题上的应用,优化了朴素解法,将时间复杂度降低到线性级别。
摘要由CSDN通过智能技术生成

单调栈

1.什么是单调栈?
从名字上就听的出来,单调栈中存放的数据应该是有序的,所以单调栈也分为单调递增栈和单调递减栈,故其分为两种
单调递增栈:单调递增栈就是从栈底到栈顶数据是从大到小
单调递减栈:单调递减栈就是从栈底到栈顶数据是从小到大

从上面的定义可以知道它就是一个栈,只不过里面存储的元素是有顺序的, 这里我们使用数组模拟栈:

#include<iostream>
#include<string>
using namespace std;
constexpr int N = 1e+5 + 10;
int st[N], tt, n;

// 这里的栈是满堆栈写法, 堆栈指针指向最后压入栈的有效数据项,此时堆栈入栈操作要先调整指针再写入数据,故这里是前置++

auto push(int x) { st[++tt] = x; }       // 入栈
auto pop() {--tt;}                       // 出栈
auto empty() -> bool { return tt == 0;}  // 判断是否为空, 初始值 tt = 0;表示为空
auto top() -> int { return st[tt]; }     // 返回栈顶元素

单调栈的题型相对来说比较固定,接下来分析几道相关题目来具体了解单调栈的作用 这里主要看单调栈的应用,因为其本身就是一个栈,在数据结构上没什么不一样的,关键是看其应用场景下如何实现元素的顺序存储.

相关练习链接 : AcWing 单调栈练习

在这里插入图片描述

#include<iostream>
using namespace std;

constexpr int N = 1e+5 + 10;
int st[N], tt, n;

auto push(int x) { st[++tt] = x; }       // 入栈
auto pop() {--tt;}                       // 出栈
auto empty() -> bool { return tt == 0;}  // 判断是否为空, 初始值 tt = 0;表示为空
auto top() -> int { return st[tt]; }     // 返回栈顶元素

auto main() -> int
{
    cin >> n;
    for(int i = 1; i <= n; ++i)
    {
        int x; cin >> x;
        while(!empty() && top() >= x) pop();   // 先判断是否为空, 若不为空切栈顶元素大于等于目前比较的数, 则出栈栈顶元素,直到栈顶元素为符合条件
        if(!empty()) cout << top() << " ";
        else cout << "-1 "; 
        push(x);                              // 此时再插入x,x此时为栈顶,是栈顶最大的元素
    }
    cout << endl;
    return 0;
}

更简便的代码

#include<iostream>
using namespace std;
constexpr int N = 1e+5 + 10;
int st[N], tt;

auto main() -> int
{
    int n; cin >> n;
    while(n--)
    {
        int a; cin >> a;
        while(tt && a <= st[tt]) --tt;
        if(!tt) cout << -1 << " ";
        else cout << st[tt] << " ";
        st[++tt] = a;
    }
}

思路分析 : 例如这样一组数据 2 3 5 4 x 现在找x左边离它最近且比它小的那个数,首先第一个条件是离它最近,故这里考虑用一下朴素做法实现我们可以直接用栈,将前面的元素依次进栈,此时栈顶元素就是左边离它最近的元素,若该元素大于等于x,遍历下一个元素比较直到找到符合条件的数,在这里我们发现5这个数子无论如何都不会作为一个正解,因为其后面有个4, 故5永远都不会是一个可选的答案,故我们可以直接将这个数字直接出栈,而不用每次都遍历一遍。 这样每一个数字最多的操作就是入栈一次和出栈一次,时间复杂度为 O(n), 而朴素做法为O(n2);

相关练习链接 : AcWing 131. 直方图中最大的矩形

在这里插入图片描述

1.确定基本思路 :
如上图所示,直方图由若干高度不同,宽度均为1的矩形组成,想要找出直方图中面积最大的矩形, 我们只要看每一个矩形其向左能扩展多少,向右能扩展多少, 以第一个矩形为例,其向左无法扩展, 向右由于2号矩形的高度小于1号矩形,故向右也无法扩展,故以第一个矩形向左向右扩展的最大矩形面积即为 h1 * 1, 由于2号矩形其高度较低,故其向左能扩展到1, 向右能扩展到7号矩形, 故以2号矩形向右向左扩展的最大面积为 h2 * 7 , 以此类推, 最后得出的是以4号矩形向左向右扩展得到的矩形面积最大。 故向左扩展向右扩展的依据就是其高度不能小于自身高度。 故一个朴素做法就是遍历每一个高度的矩形,然后向左遍历直到遇到一个高度小于目前所要扩展的矩形的高度, 向右同理。 这种朴素做法最坏的时间复杂度为 o(n2), 这里数据的个数范围 1 ~ 100000 , 故会超时 。

在这里插入图片描述

2.单调栈进行优化
跟上面的类似, 这里以向左扩展为例,上面基本思路已经分析了,向左扩展的终止条件就是找到一个高度小于自己的矩形, 以上面为例,当需要找到一个矩形左边高度比它小的矩形,这时遍历的过程中像2号矩形和3号矩形就不需要进行遍历,因为4号矩形的高度比2和3都要低,且在2和3的右边, 故2和3绝对不会作为一个正解,故此时我们并不需要遍历2和3,即我们用栈并不需要存储2号和3号。

#include<iostream>
using namespace std;
constexpr int N = 1e+5 + 10;
long long h[N], l[N], r[N], st[N], tt, n;    // h保存高度, l数组保存每个矩形向左扩展到的位置, r数组表示没给矩形向右扩展到的位置  st入栈的是每个矩形的位置 

auto main() -> int
{
    while(cin >> n, n)         // 题目说明有n组测试数据, 当 n 为 0 时退出
    {
        h[0] = h[n + 1] = -1;  // 定义左右边界 0 和 n + 1 其高度为 -1, 直方图中矩形的高度范围为 0 ~ 1000000000        
        for(int i = 1; i <= n; ++i) cin >> h[i];
         
        tt = 0;                // 满堆栈写法 
        st[tt] = 0;            // 栈顶初始化为直方图的左边界 

		// 向左扩展 
        for(int i = 1; i <= n; ++i)       
        {
            while(h[i] <= h[st[tt]]) --tt;    // 若当前高度小于栈顶高度向左扩展,栈顶元素弹出
            l[i] = st[tt];                    // 此时栈顶的高度为小于当前矩形的高度
            st[++tt] = i;                     // 将当前矩形的位置入栈
        }
        
		// 同理 向右扩展  
        tt = 0;
        st[tt] = n + 1;
        for(int i = n; i ; --i)               // 从右往左遍历 
        {
            while(h[i] <= h[st[tt]]) --tt;    
            r[i] = st[tt];
            st[++tt] = i;
        }
        
        long long res = -1; 
        
        // 遍历每个矩形,计算出其向左向右扩展后的矩形最大值. 
        for(int i = 1; i <= n; ++i) res = max(res, h[i] * (r[i] - l[i] - 1));   
        cout << res << endl;                    
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值