单调栈

//这是19号的话~拖了一天了
以后不能随便发pyp了//
我们还是很水的,hduoj做到最后脑壳疼
现在还是先把今天看的单调栈做个小小的总结叭

  • 例题
  • HDU 1506
  • HDU 5033
  • PKU 2796
  • PKU 3250

今天先以HDU 1506为例

定义

单调递增或单调减的栈,跟单调队列差不多,但是只用到它的一端,利用它可以用来解决一些ACM/ICPC和OI的题目,如RQNOJ 的诺诺的队列等。

单调栈是一种特殊的栈,特殊之处在于栈内的元素都保持一个单调性。
假设下图是一个栈内元素的排列情况(单调递增的栈):


此时插入情况有两种:
(1).插入元素大于栈顶元素
当插入7时,因7 > 6,满足单调递增的条件,故可以直接加入栈
此时:


单调栈_第1张图片

(2).插入的元素小于栈顶元素
当插入3时,为了满足单调递增栈的性质,需要先将栈顶的4,6弹出,再插入,此时:


结论

利用单调栈,可以找到从左/右遍历第一个比它小/大的元素的位置

栗子来啦:
假设有一个单调递增的栈 S和一组数列:
a : 5 3 7 4

用数组L[i] 表示 第i个数向左遍历的第一个比它小的元素的位置

如何求L[i]?

首先我们考虑一个朴素的算法,可以按顺序枚举每一个数,然后再依此向左遍历。
但是当数列单调递减时,复杂度是严格的O(n^2)。

此时我们便可以利用单调栈在O(n)的复杂度下实现

我们按顺序遍历数组,然后构造一个单调递增栈

(1). i = 1时,因栈为空,L[1] = 0,此时再将第一个元素的位置下标1存入栈中

此时栈中情况:


(2).i = 2时,因当前3小于栈顶元素对应的元素5,故将5弹出栈
此时栈为空
故L[2] = 0
然后将元素3对应的位置下标2存入栈中

此时栈中情况:


单调栈_第2张图片

(3).i = 3时,因当前7大于栈顶元素对应的元素3,故
L[3] = S.top() = 2 (栈顶元素的值)

然后将元素7对应的下标3存入栈
此时栈中情况:


单调栈_第3张图片

(4).i = 4时,为保持单调递增的性质,应将栈顶元素3弹出 (我认为他这里的3是说第三个也就是元素7,不然就不对了)
此时 L[4] = S.top() = 2;

然后将元素4对应的下标3存入栈
此时栈中情况:


对应的结果:
a : 5 3 7 4
L : 0 0 2 2

总结:

一个元素向左遍历的第一个比它小的数的位置就是将它插入单调栈时栈顶元素的值,若栈为空,则说明不存在这么一个数。然后将此元素的下标存入栈,就能类似迭代般地求解后面的元素

实现

1.最基础的应用就是给定一组数,针对每个数,寻找它和它右边第一个比它大的数之间有多少个数。

2.给定一序列,寻找某一子序列,使得子序列中的最小值乘以子序列的长度最大。

3.给定一序列,寻找某一子序列,使得子序列中的最小值乘以子序列所有元素和最大。

代码模板

Stack S;
    for(int i=1 ;i<=n ;i++){
        while(S.size() && a[S.top()] >= a[i]) S.pop();

        if(S.empty())     L[i] = 0;
        else              L[i] = S.top();

        S.push(i);
    }

例题:HDU 1506

题目链接:

首先考虑最大面积的矩形X的左右边界的性质:
设其左边界为L,右边界为R,则其高H = min{h[i] | L <= i <= R}
此时最大面积为 (R - L + 1) * H
若此时左边界的左边那个矩形的高度 h[L-1] >= H
则左边界可以向左拓展,则新的面积为:
(R - (L-1) + 1) * H > 原面积
则与原假设条件冲突
故左边界左边的那个矩形的高度 :h[L-1] < H
同理右边界右边的那个矩形的高度: h[R+1] < H
设H = h[i]
所以左边界L是满足h[j-1] < h[i]的最大的j,即从i点向左遍历的第一个高度比i小的点的右边一个点
而右边界R是满足 h[j+1] < h[i]的最小的j,即从i点向右遍历第一个高度比i小的点的左边一个点
所以我们可以利用单调栈的性质得到每个确定点,即确定高度的最大面积矩形的左右边界,然后枚举取最大即可。

现在最主要的问题是(R[i]-L[i])是对应下标为i的矩形的宽度,那R[i] 和 L[i]分别表示什么?

#include 
using namespace std;
typedef long long ll;      //   这些做法可以学习
//typedef unsigned long long ull;

const int N= 100000+100;

stackS;
ll h[N];        //这里的ll要小心,不然就会wrong
int R[N],L[N];
//L和R都是用来储存坐标,我要他从小到大排序,如果遇到了一个小的,就将之前比他大的全部出栈

int main()
{
    int n;
    freopen("data","r",stdin);
    while(cin>>n&&n>0){
        for(int i=0;i>h[i];
        while(S.size()) S.pop();
        for(int i=0;i=h[i])
                S.pop();
            if(S.empty())   L[i]=0;            
            else    L[i]=S.top()+1;         
            S.push(i);
        }
        for(int i=0;i=0;i--){
            while(S.size()&&h[S.top()]>=h[i])
                S.pop();
            if(S.empty())   R[i]=n;
            else    R[i]=S.top();           //为什么这个不用+1,因为i所对应的位置就是栈顶此刻对应的位置,无形中已经+1
            S.push(i);
        }
        for(int i=0;i

关于模板我刚开始不理解这道题,就去借鉴了一下https://blog.csdn.net/zuzhiang/article/details/78134247 结果发现他对这道题的代码并不是很能理解,而且感觉他少了一边。然后又回来了这个代码

其实L和R记录的是元素i向左向右最长能到达的那个元素,可能再-1,这是开区间闭区间的区别。

假*单调栈

后来参考了一下师兄的,学会了一个假*单调栈
就拿这个例题的样例为例叭
input 2 1 4 5 1 3 3

先默认每个数的最左边就是左边的那个数,但是一旦遇到一个更小的数,它的左边界就不仅仅是左边最靠近它的那个数了,这时得循环,它可能是左边的左边界,甚至一直左边界下去,比如:L[5]=1,所以L[5]=L[L[5]],而L[5]其实就是下标4,向左移一位嘛,同理R的话是向右移一位。

代码如下:

#include 
#include 
#include 
#include 
#define ll long long
using namespace std;
ll L[100001],R[100001],h[100001];

int main()
{   
    long long int n;
    freopen("data","r",stdin);
    while(scanf("%lld",&n)&&n){
        for(int i=1;i<=n;i++)
            scanf("%lld",&h[i]);
        for(int i=1;i<=n;i++){
            L[i]=i-1;
            R[i]=i+1;
        }
        for(int i=1;i<=n;i++)
            while(L[i]&&h[L[i]]>=h[i])
                L[i]=L[L[i]];
        for(int i=n;i>0;i--)
            while(R[i]<=n&&h[R[i]]>=h[i])
                R[i]=R[R[i]];
        long long int sum=0;
        for(int i=1;i<=n;i++)
            sum=sum>(h[i]*(R[i]-L[i]-1))?sum:(h[i]*(R[i]-L[i]-1));
        printf("%lld\n",sum);
    }
    return 0;
}

义无反顾入坑

  • n,h,R,L要是long long 型
  • sum=sum>(h[i](R[i]-L[i]-1))?sum:(h[i](R[i]-L[i]-1));这个如果不确定的话可以自己去瞎掰模拟一下//虽然会浪费时间

你可能感兴趣的:(单调栈)