[数据结构]单调栈的基本应用

[数据结构]单调栈的基本应用

一、单调栈的实现

1.定义

简单来说,单调栈是一种保证其内部元素单调递增或单调递减的数据结构。
即单调栈维护了由栈底到栈顶元素的单调性。

2.实现方法

对于单调下降栈:

a. 若当前栈为空,无条件入栈;

b. 若栈顶元素大于待入栈元素时,入栈;

c. 若栈顶元素小于等于待入栈元素时,不断弹出栈首元素,直到栈顶元素大于待入栈元素或栈为空为止,并将该元素入栈;

下面给出单调下降栈的核心代码:

for(int i=1;i<=n;i++){
    scanf("%d",&a);
    while(top&&a>=Stack[top]) top--;
    //求单调上升栈只须将">="改为"<="
    Stack[++top]=i;
}

二、单调栈的基本应用

单调栈的优势在于其复杂度为O(n),原因是每个元素只会进栈一次、出栈一次。
以下为单调栈的基本应用:

求出序列a内a[i]的左区间或右区间第一个大于或小于a[i]的值的位置

例1: P2947 [USACO09MAR]向右看齐Look Up

题目描述:给定一个序列a,求ai的右区间第一个大于ai的位置。
思路:我们如果暴力枚举的话,复杂度为\(O(n^2)\),必然会超时。因此我们使用单调栈。
从左至右扫描整个序列并维护一个严格不上升栈(因为题目说明了只有当hi>hj时奶牛i才会仰望奶牛j)。
代码:

#include<bits/stdc++.h>
#define N 120000
using namespace std;
int h,n,top,ans[N];
struct node{
    int pos,h;
}q[N];
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&h);
        while(top&&h>q[top].h){
            ans[q[top].pos]=i;
            top--; 
        }
        q[++top]=(node){i,h};
    }
    for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
    return 0; 
}

例2: P1901 发射站

题目描述:给定一个序列a,求ai的左区间和右区间第一个大于ai的位置的权值和。
思路:我们既可以考虑ai是由哪些点拼凑出来的,也可以考虑组成ai的答案有哪些因素。
对于后者,我们很容易联想到单调栈。每当弹入一个新元素,如果大于栈顶元素,那么不断弹出栈顶元素并把权值累加到新元素上。如果新元素小于栈顶元素,那么将新元素的权值累加到栈顶元素上。像这样不断操作,就能得到最终答案了。
代码:

#include<bits/stdc++.h>
#define ll long long
#define N 1000010
using namespace std;
ll Stack[N],n,top,ans[N];
struct node{
    ll h,v;
}p[N];
int main()
{
    scanf("%lld",&n);
    for(int i=1;i<=n;i++){
        scanf("%lld%lld",&p[i].h,&p[i].v);
        while(top&&p[i].h>p[Stack[top]].h){
            ans[i]+=p[Stack[top]].v;
            top--;
        }
        ans[Stack[top]]+=p[i].v;
        Stack[++top]=i;
    }
    ll res=0;
    for(int i=1;i<=n;i++) res=max(res,ans[i]);
    printf("%lld",res);
    return 0;
}

例3: P2659 美丽的序列

题目描述:求一个序列(任意区间*该区间的min)的和。
思路:一道非常好的单调栈的题。同样的,我们既可以枚举所有的区间来计算最后的答案。显然,我们无法承受这样巨大的时间复杂度。相反,如果我们考虑序列中一个数作为min时能影响的区间范围,在乘上这个数,便能得到我们所要的答案。此时,问题便转化为求ai右边和左边第一个小于ai的位置。而上升栈恰擅长求解这类问题。具体解释详见代码。
代码(这里提供另一种写法):

#include<bits/stdc++.h>
#define ll long long
#define N 2000010
using namespace std;
int Stack[N],top,n;
ll a[N],l[N],r[N],ans;
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    //分别从左扫描,右扫描,求出ai最大影响区间 
    for(int i=1;i<=n;i++){ 
        while(top&&a[Stack[top]]>=a[i]) l[i]+=l[Stack[top--]];
        //若新元素的值比栈顶要小,此时该元素能延伸到的左区间范围必然会包含栈顶元素延伸的范围
        //而此时栈内元素是单调上升的,故累加的左区间不会重叠而保证正确性 
        l[i]++;
        Stack[++top]=i;
    }
    top=0;
    for(int i=n;i>=1;i--){
        while(top&&a[Stack[top]]>=a[i]) r[i]+=r[Stack[top--]];
        //同上,只是扫描方向相反而已 
        r[i]++;
        Stack[++top]=i;
    }
    for(int i=1;i<=n;i++)//注意影响区间是(li+ri-1) 
        ans=max(ans,(l[i]+r[i]-1)*a[i]);
    printf("%lld\n",ans);
    return 0;
}

当然,这里也提供上一题类似的写法:

for(int i=1;i<=n;i++) l[i]=0,r[i]=n+1;//赋初值
for(int i=1;i<=n;++i){
    while(top&&a[Stack[top]]>=a[i]) r[Stack[top]]=i,top--;//栈顶元素被新元素所影响
    l[i]=Stack[top];//此时新元素不在有能力影响此时栈顶的元素,可以确定li
    Stack[++top]=i;
}

例4: P1823 [COI2007] Patrik 音乐会的等待

题目描述:本题为发射站的加强版本。对于给定的序列a,求出比ai大的值的第一个位置。
思路:为了避免计数重复,我们需要从右向左寻找第一个大于ai的位置。由于单调栈具有二分性,所以我们只需要在单调下降栈内二分找到这个值就可以了。最后只需要累计答案即可。
代码:

#include<bits/stdc++.h>
#define N 500010
#define ll long long
using namespace std;
ll ans;
int Stack[N],a[N],n,top;
inline void erfen(int x){
    int l=0,r=top,res=0;
    while(l<=r){
        int mid=(l+r)>>1;
        if(a[Stack[mid]]>x){
            l=mid+1;
            res=mid;
        }
        else r=mid-1;
    }
    if(res>0) ans+=top-res+1;
    else ans+=top; 
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=1;i<=n;i++){
        erfen(a[i]);
        while(top&&a[i]>a[Stack[top]]) top--;
        Stack[++top]=i;
    }
    printf("%lld",ans);
    return 0;
}

三、小结

1.我们维护单调上升栈来找到第一个小于ai的位置
2.我们维护单调下降栈来找到第一个大于ai的位置
3.我们可以利用单调栈求出ai为min或max最大的影响范围

猜你喜欢

转载自www.cnblogs.com/cyanigence-oi/p/11706494.html
今日推荐