单调栈

 单调栈:

单调栈解决的是:以某个值为最小(最大)值的最大区间。

实现方法:

求最小值(最大值)的最大区间,维护一个递减(递增)的栈。(下面以求最小值最大区间为例)

当遇到一个比栈顶小的值的时候开始弹栈,弹栈停止的位置到这个值的区间即为此值左边的最大区间;同时,当一个值被弹掉的时候也就意味着比它更小的值来了,也可以计算被弹掉的值的右边的最大区间。

单调递增:数据出栈的序列为单调递增序列(即从栈顶到栈底的元素是单调递增的)

单调递减:数据出栈的序列为单调递减序列(与上面相反)

 模板如下:

 1 stack<int> sta;
 2 for (遍历这个数组)
 3 {
 4     while(栈不为空 && 栈顶元素小于当前元素){
 5         更新结果;
 6         栈顶元素出栈;
 7     }
 8     if(栈空 || 栈顶元素大于等于当前比较元素){
 9         当前数据入栈;
10     }
11 }

 或

 1 stack<int> sta;
 2 for (遍历这个数组)
 3 {
 4     if(栈空 || 栈顶元素大于等于当前比较元素){
 5         入栈;
 6     }
 7     else{
 8         while(栈不为空 && 栈顶元素小于当前元素){
 9             更新结果;
10             栈顶元素出栈;
11         }
12         当前数据入栈;
13     }
14 }
15                             

 /*如果要使最后单调栈内的元素能全部弹出,一般要在数组最后加一个符合条件的值,使栈内元素能全部弹出。*/

例题:Largest Rectangle in a Histogram

题意

求所给出的条形图可形成的最大矩形面积。

题解

以每一个矩形的高度作为最终大矩形的高度,看最宽能是多少,然后统计最优解。(暴力法O(n^2)超时)

维护单调栈(单调递减),当前高度数值大于栈顶元素大小则入栈,小于则出栈。在维护单调性的弹出操作时统计宽度,不断更新答案即可得到最优解。

即为求以每个高度为最小值的最大宽,计算形成的矩形面积,不断更新答案求最大面积值。

Code1(用STL的stack)

具体做法:当遇到一个比栈顶小的值的时候开始准备弹栈,此时意味着栈顶的值遇到了比它小的值,所以当前位置即为以该栈顶即将弹出的值为最小值的区间最右端;每个当前准备入栈的值的左端点初始化为当前位置,当需要弹栈时,弹栈最后停止的位置更新为当前准备入栈的值的左端点;故每弹一次栈即可求以弹栈值为最小值的最大区间。(注意得到的区间是为左闭右开的)

#include<cstdio>
#include<stack>
using namespace std;
typedef long long ll;
const int maxn=1e5+5;
ll h[maxn];
int main()
{
    ll n;
    while(~scanf("%lld",&n)&&n){
        for(int i=1;i<=n;i++){
            scanf("%lld",&h[i]);
        }
        h[n+1]=0;//为了让栈内的元素最后能全部弹出,令h[n+1]=0。因为是多测试用例,所以这里必须重新初始化,否则可能会受前一次测试用例影响
        ll area,Max=-1;
        stack<pair<ll,ll> >S;//第一个存高度值,第二个存当前高度可达到的最左端位置
        for(int i=1;i<=n+1;i++){
            ll L=i;//可达的最左端位置L初始化为当前位置
            while(!S.empty()&&h[i]<=S.top().first){ //出栈
                L=S.top().second;//为当前准备入栈的高度值更新可达的最左端位置L
                area=(i-L)*S.top().first;//L为矩形左边界,当前i为右边界,i-L为矩形的宽,S.top().first为矩形的高
                Max=max(area,Max);
                S.pop();
            }
            if(S.empty()||h[i]>S.top().first)//入栈
                S.push({h[i],L});
        }
        printf("%lld
",Max);
    }
    return 0;
}
View Code

 Code2:手写栈

写法不同于上面,但根源思路其实是一样的。

/*AC 109ms*/
#include<cstdio>
#include<algorithm>
#include<cstring>
typedef long long ll;
const int MAX=1e5+5;
using namespace std;
ll h[MAX],w[MAX],stack[MAX];
int main()
{
    int n;
    while(scanf("%d",&n)&&n)
    {
        memset(h,0,sizeof(h));//多测试用例,必须要重新初始化
        ll ans=0,k;///k为右宽
        for(int i=1;i<=n;i++){
            scanf("%lld",&h[i]);
        }
        
        h[n+1]=0;//为了方便把最后剩下的单调递增的矩形也统计进去,我们假设h[n+1]的位置有一个高度为0的矩形,最后将它加入单调栈时他会将所有矩形都弹出,那么答案也就完成最后的更新了。
        int top=0;
        stack[0]=-1;///注意这里要将单调栈的0下标位置初始化为-1,否则有可能会在下面while处陷入死循环导致超时
        
        for(int i=1;i<=n+1;i++){
            if(h[i]>stack[top]){///严格单调递减
                stack[++top]=h[i];///入栈
                w[top]=1;//左宽为1,即该入栈元素本身
            }
            else{
                k=0;///第一个出栈元素右宽为0
                while(h[i]<=stack[top]){
                    w[top]+=k;///算出该出栈元素的总宽(左宽+右宽)
                    ans=max(stack[top]*w[top],ans);///计算以该出栈元素为高的矩形面积,更新最优解
                    k=w[top];///下一个出栈元素的右宽为上一个出栈元素的总宽
                    top--;
                }
                stack[++top]=h[i];///入栈
                w[top]=1+k;//该入栈元素左宽为最后一个出栈元素的总宽+1
            }
        }
        printf("%lld
",ans);
    }
    return 0;
}
View Code

题目:Feel Good

题意

给出正整数n和一个(1 <= n <= 100 000)长度的数列,要求找出一个子区间,使这个子区间的数字和乘上子区间中的最小值的结果最大。输出这个最大值与区间的两个端点。

题解

前缀和+单调栈,和上面的题原理一样,具体看代码

Code

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e5+5;
int main()
{
    ll n;
    while(~scanf("%lld",&n))
    {
        ll a[maxn]={},sum[maxn]={};
        for(int i=1;i<=n;i++){
            scanf("%lld",&a[i]);
            sum[i]=sum[i-1]+a[i];
        }
        a[n+1]=-1;
        stack<pair<ll,ll> >sta;
        ll ans=-1,l,r;
        for(int i=1;i<=n+1;i++){
            ll Left=i;
            while(!sta.empty()&&a[i]<=sta.top().first){
                if((sum[i-1]-sum[sta.top().second-1])*sta.top().first>ans){
                    ans=(sum[i-1]-sum[sta.top().second-1])*sta.top().first;
                    l=sta.top().second;//记录最大结果的左右端点
                    r=i-1;
                }
                Left=sta.top().second;
                sta.pop();
            }
            sta.push({a[i],Left});
        }
        printf("%lld
",ans);
        printf("%lld %lld
",l,r);
    }
    return 0;
}
View Code
原文地址:https://www.cnblogs.com/HOLLAY/p/11354622.html