单调栈与NGE问题

引出

同样地见模板题

【模板】单调栈

题目描述

给出项数为 n n n 的整数数列 a 1 … n a_{1 \dots n} a1n

定义函数 f ( i ) f(i) f(i) 代表数列中第 i i i 个元素之后第一个大于 a i a_i ai 的元素的下标,即 f ( i ) = min ⁡ i < j ≤ n , a j > a i { j } f(i)=\min_{i<j\leq n, a_j > a_i} \{j\} f(i)=mini<jn,aj>ai{j}。若不存在,则 f ( i ) = 0 f(i)=0 f(i)=0

试求出 f ( 1 … n ) f(1\dots n) f(1n)

输入格式

第一行一个正整数 n n n

第二行 n n n 个正整数 a 1 … n a_{1\dots n} a1n

输出格式

一行 n n n 个整数 f ( 1 … n ) f(1\dots n) f(1n) 的值。

样例 #1

样例输入 #1

5
1 4 2 3 5

样例输出 #1

2 5 4 5 0

提示

【数据规模与约定】

对于 30 % 30\% 30% 的数据, n ≤ 100 n\leq 100 n100

对于 60 % 60\% 60% 的数据, n ≤ 5 × 1 0 3 n\leq 5 \times 10^3 n5×103

对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 3 × 1 0 6 1 \le n\leq 3\times 10^6 1n3×106 1 ≤ a i ≤ 1 0 9 1\leq a_i\leq 10^9 1ai109

分析

单调栈可以解决NGE问题,也就是Next Greater Element问题,这个问题就是上面的模板题,可以阅读题面来理解。当然了,对算法稍加改动,我们还可以求NSE(Next Smaller Element)。

这里先给出结论:单调栈可以在 O ( n ) O(n) O(n)地解决序列中每个元素的前面/后面的比它大/小的第一个值的问题

  • NGE,下一个比它大的数
  • NSE,下一个比它小的数
  • LGE,上一个比它大的数
  • LSE,上一个比它小的数

为什么要引出单调栈?我们先考虑上面问题的补素做法:遍历每一个数,同时再使用一个循环对其后面的数进行扫描,直到找到比它大的数。考虑构造一个单调递减序列,上面的解法就会退化到 O ( n 2 ) O(n^2) O(n2)的复杂度

虽然先解决上一个比它大/小的数的问题更容易一些(符合思维习惯顺序),但是我们还是就着模板题来讲解

其实与单调队列相比单调栈的思路更加简单,也不需要对其使用条件做太多的思考。与单调队列类似,我们可以舍弃之前遍历到的数(已经用栈维护)中不会对后面的答案产生影响的数,在NGE问题中,我们只要舍弃比当前数小的元素就可以了。当然需要注意的是,为了得到每个数之后第一个比它大的数,我们需要先维护那部分数据,所以应该是逆序遍历。那么既然是逆序遍历,如果一个数比它右边所有的数都要大,那么它就“挡住”了右边的数,左边的数自然是看不到它后面的数的,右边的数就没用需要舍弃了。然后我们将当前数使用栈来维护。还需要注意存储下标更为方便,这在单调队列的文章中已经强调了,其它的也大同小异甚至更加简单,为什么请看下一小节

与单调队列的关系

理解单调队列还需要理解每个元素的入队顺序和在队中顺序及大小的单调性的问题,但在单调栈中不必了,因为单调栈的方向也可以说是它维护的数的区间的移动方向是固定的,单调栈只有栈顶可以进出元素,而单调队列的队尾和队头都可以进出元素!所以单调栈可以看作是一种特殊的单调队列。自然而然,单调栈能做的单调队列也一定能做,单调栈的代码也更为简单

模板题代码

使用stl栈版本

// Problem: P5788 【模板】单调栈
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P5788
// Memory Limit: 125 MB
// Time Limit: 1000 ms

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pi;
const int N = 3e6 + 10;
const int MOD = 1e9 + 7;
#define endl '\n'
#define PY puts("Yes")
#define PN puts("No")
int a[N], ans[N];
stack<int> stk;
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    for (int i = n; i >= 1; i--)
    {
        while (!stk.empty() && a[i] >= a[stk.top()])
            stk.pop();
        if (!stk.empty())
            ans[i] = stk.top();
        stk.push(i);
    }
    for (int i = 1; i <= n; i++)
        cout << ans[i] << " ";
    return 0;
}

手写栈(比stl更快)

// Problem: P5788 【模板】单调栈
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P5788
// Memory Limit: 125 MB
// Time Limit: 1000 ms

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pi;
const int N = 3e6 + 10;
const int MOD = 1e9 + 7;
#define endl '\n'
#define PY puts("Yes")
#define PN puts("No")
int a[N], ans[N];
int stk[N];
int top;
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    for (int i = n; i >= 1; i--)
    {
        while (top && a[i] >= a[stk[top]])
            top--;
        if (top)
            ans[i] = stk[top];
        stk[++top] = i;
    }
    for (int i = 1; i <= n; i++)
        cout << ans[i] << " ";
    return 0;
}

例题 接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

在这里插入图片描述

示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:

输入:height = [4,2,0,3,2,5]
输出:9

提示:
n == height.length
1 <= n <= 2 * 1e4
0 <= height[i] <= 1e5


这道题方法很多,我们就给出原始的单调栈解法
接雨水的瓶颈在于两端的短板,我们找到了一个短板就确定了这段区间内最大雨水高度,然后我们利用单调栈来找出比它高的板子,给答案加上这两块板子之间的高度
我们分别维护一个NGE栈和一个LGE栈,也就是正着扫一遍反着扫一遍,因为只从一遍扫可能少统计
由于是在维护单调栈过程中加答案,需要另外维护一个数组来保存已经填充的水的高度,避免答案加上出现重复的水

class Solution {
public:
    int trap(vector<int>& height) 
    {
        stack<int> s1,s2;
        vector<int> filled(height);
        int ans=0;
        for (int i=height.size()-1;i>=0;i--)
        {
            while (s1.size()&&height[i]>height[s1.top()])
                s1.pop();
            if (s1.size())
            {
                for (int j=i+1;j<s1.top();j++)
                {
                    ans+=(height[i]-filled[j]);
                    filled[j]=height[i];
                }
            }
            s1.push(i);
        }
        for (int i=0;i<height.size();i++)
        {
            while (s2.size()&&height[i]>height[s2.top()])
                s2.pop();
            if (s2.size())
            {
                for (int j=s2.top()+1;j<i;j++)
                {
                    if (filled[j]>height[i])
                        continue;
                    ans+=(height[i]-filled[j]);
                    filled[j]=height[i];
                }
            }
            s2.push(i);
        }
        return ans;
    }
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值