单调栈的使用(带例题、由易到难)

前言

这篇文章用三道例题解释下单调栈

单调栈是什么

栈里的元素 严格或非严格 单调递增或递减
实现时要维护栈内的元素有序

例题一 Acwing1978(简单)

题目链接 https://www.acwing.com/problem/content/description/1980/

题目

每天,农夫约翰的 N头奶牛都会穿过农场中间的马路。考虑约翰的农场在二维平面的地图,马路沿水平方向延伸,马路的一侧由直线 y=0描述,另一侧由直线 y=1描述。奶牛 i从马路一侧的位置 (ai,0) 沿直线过马路到达另一侧的位置 (bi,1)。
所有 ai互不相同,所有 bi互不相同。
尽管他的奶牛们行动敏捷,他还是担心行动路径交叉的两头奶牛在过马路时发生碰撞。
约翰认为,如果一头奶牛的行动路径没有跟其他任何奶牛的行动路径相交,则该奶牛是安全的。
请帮助约翰计算安全奶牛的数量。

输入格式
第一行包含整数 N。
接下来 N行,每行包含两个整数 ai,bi,用来描述一头牛的行动路径。

输出格式
输出安全奶牛的数量。

数据范围
1 ≤ N ≤ 1 0 5 , − 1 0 6 ≤ a i , b i ≤ 1 0 6 1≤N≤10^5,−10^6≤a_i,b_i≤10^6 1N105,106ai,bi106

输入样例

4
-3 4
7 8
10 16
3 9

输出样例

2

样例解释

第一头牛和第三头牛的行动路线不与其他奶牛的路线相交。
第二头牛和第四头牛的行动路线相交。

题目分析

排序必不可少、之后用到了单调栈(严格单调递增栈)的算法:

  1. 新加的元素如果比最大值大、它一定可以被加进来
  2. 新加的元素如果比最大值小、有可能出现交集的情况、通过删除栈内元素维持栈的单调递增的特性

代码

#include <bits/stdc++.h>

using namespace std;
struct Node
{
    int x;
    int y;
};

bool cmp(Node &p1, Node &p2)
{
    return p1.x < p2.x;
}

int main()
{
    int n; cin >> n;
    Node a[n];
    for (int i = 0; i < n; i ++ ) scanf("%d%d", &a[i].x, &a[i].y);
    sort(a, a + n, cmp); //按x从小到大排序、进栈元素为y
    
    stack<int> sta; //单调严格递增栈
    int maxn = -1e7;
    for(int i = 0; i < n; ++ i)
    {
        if(maxn < a[i].y) sta.push(a[i].y);  //新进来的不会和原来的有交集
        else
            while(sta.size() && sta.top() > a[i].y) //不然可能有交集、将相交的部分全部清空
                sta.pop();
        maxn = max(maxn, a[i].y); //每次更新下最大值
    }
    
    cout << sta.size();
    return 0;
}

例题二 力扣402(中等)

题目链接 https://leetcode-cn.com/problems/remove-k-digits/

题目

给你一个以字符串表示的非负整数 num 和一个整数 k ,移除这个数中的 k 位数字,使得剩下的数字最小。请你以字符串形式返回这个最小的数字。

示例 1 :

输入:num = “1432219”, k = 3
输出:“1219”
解释:移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219 。

示例 2 :

输入:num = “10200”, k = 1
输出:“200”
解释:移掉首位的 1 剩下的数字为 200. 注意输出不能有任何前导零。

示例 3 :

输入:num = “10”, k = 2
输出:“0”
解释:从原数字移除所有的数字,剩余为空就是 0 。

提示:

1 <= k <= num.length <= 105
num 仅由若干位数字(0 - 9)组成
除了 0 本身之外,num 不含任何前导零

题目分析

一个直观的思路就是将出现在前面且大的数字删掉。是一个单调栈思想。将字符串的右边看作栈的开口,字符串左边封闭。

围绕这个核心完善细节:

  1. 每预加一个数时将它前面出现的比它大的删去、删一个减一次总次数
  2. 有前导零的情况:清空
  3. 最后遍历一遍后任可能总次数未被减到0,从后往前删除
  4. 最后如果为空返回0

代码

class Solution {
public:
    string removeKdigits(string num, int k) {
        //升序
        int n = num.size();
        string s;  //将字符串抽象成栈,左边是栈底、右边是带有开口的栈顶
        for(int i = 0; i < n; ++ i)
        {
            //核心思想就是将出现在前面的大的数删去
            while(k && s.size() && s.back() > num[i]) s.pop_back(), k --; //每次减总次数
            if(s == "0") s = ""; //去除前缀0
            s.push_back(num[i]); //将当前加入答案
        }
        while(k && s.size()) s.pop_back(), k--;  //此时的栈是一个非严格单调递增、从后往前删
        if(!s.size()) return "0"; //特判
        return s;
    }
};

例题三 力扣84(困难)

题目链接 https://leetcode-cn.com/problems/largest-rectangle-in-histogram/

题目

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 10

数据范围

  • $ 1 <= heights.length <=10^5 $
  • 0 < = h e i g h t s [ i ] < = 1 0 4 0 <= heights[i] <= 10^4 0<=heights[i]<=104

题目分析

暴力做法是枚举每一个点、将这个点向两边双指针遍历、遇到小于它的元素时终止双指针遍历。但是在极端情况下该做法的时间复杂度高达 O ( n 2 ) O(n^2) O(n2) ,是个效率堪忧的算法。

栈的特点是什么?先进后出。从左到右遍历的过程中每个可能的点向左寻找答案,符合栈的特性。

核心思想是向右遍历过程中维护一个非严格单调递增栈,当不能确定答案时将下标入栈、能够确定答案时将所有不必要数据出栈。

一些细节:

  1. 栈只用存下标

  2. 最后要拟入栈一个比所有数小的数字将栈清空

  3. 带有一点点贪心策略:如原栈是 134 134 134,拟入栈 2 2 2 3 3 3 4 4 4在之后高度最高只能到 2 2 2了,要更新下原数组的高度

代码

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        heights.push_back(-1);  //确保在最后将栈清空
        int n = heights.size();
        stack<int> sta;
        int ans = 0, tmp;
        for(int i = 0; i < n; ++ i)
        {
            if(sta.empty() || heights[i] >= heights[sta.top()]) sta.push(i); //不能确定答案、先入栈
            else // 能确定答案了
            {
                int idx;
                while(sta.size() && heights[i] < heights[sta.top()]) //维护栈的非严格单调递增
                {
                    idx = sta.top();
                    sta.pop();
                    ans = max(ans, heights[idx] * (i - idx)); //更新答案
                }
                sta.push(idx); //将拟入栈的入栈
                heights[idx] = heights[i]; //改变原数组的数据大小,只能这么高了
            }
        }
        return ans;
    }
};

总结

单调栈的核心是维护栈内数据有序。在满足有序的前提下直接入栈;不满足的话通过删除栈内数据达到有序。

伪代码大致是:

for(int i = 0; i < n; ++ i)  //从左到右遍历一遍
{
    if(sta.empty() || f1) sta.push(i); //直接入栈  f1是满足某条件   
    else //不然的话维护栈内元素有序
    {
        while(sta.size() && f2) //只要不能有序就一直删
        {
            sta.pop(); //通过删除来满足栈内有序
            x1; //执行的其它语句
        }
    }
    x2; //根据题目要求写
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值