【栈】单调栈经典例题

一.题目

        1.题目描述

现在有一个长度为n的序列,在形成的任意子区间中,查找最小的值和此区间和的乘积的最大值。

        2.样例输入

6

3 1 6 4 5 2

        3.样例输出

60

        4.解析

例如在区间1-6中,最小值为1,区间和为21,积为21;

在区间3-5中,最小值为4,区间和为15,积为60;

再由遍历其他区间,得出值60为最大,最后您输出了60。

 二.暴力算法(O(n^{3}))

#include<iostream>
#include<cmath>

using namespace std;
const int MAX=2147483647; //定义一个极大值
int a[1001]; //定义一个长度为1001的数组,用来存储序列
int sum[1001]; //定义一个长度为1001的数组,用来存储序列的前缀和
int n; //定义一个变量,用来存储序列的长度

int MIN(int i,int j){ //定义一个函数,用来求序列中i到j之间的最小值
    int minn=MAX; //初始化最小值为极大值
    for(int k=i;k<=j;k++){ //遍历i到j之间的所有元素
        minn=min(a[k],minn); //更新最小值
    }
    return minn; //返回最小值
}

int main(){
    cin>>n; //读入序列的长度
    for(int i=0;i<n;i++) cin>>a[i]; //读入序列的元素值
    int summ=0; 
    for(int i=0;i<n;i++){ //计算序列的前缀和
        summ+=a[i];
        sum[i]=summ;
    }
    int maxx=0; //初始化最大值为0
    //头 
    for(int i=0;i<n-1;i++){ //枚举所有可能的子区间头部
        //尾 
        for(int j=i+1;j<n;j++){ //枚举所有可能的子区间尾部
            int s=MIN(i,j); //计算子区间的最小值
            int d=sum[j]-sum[i-1]; //计算子区间的和
            int g=d*s; //计算最小值和区间和的乘积
            maxx=max(maxx,g); //更新最大值
        }
    } 
    cout<<maxx<<endl; //输出最大值
    return 0;
}

三.思路分析

分析上面的暴力枚举不难发现,我们是通过枚举每个区间之后,从头到尾再寻找区间最小值和通过前缀和找到的乘积。

那我们不妨想一想,这一组数据,我们是不是可以先找到最小值再来确定区间,然后找到区间和进行计算,这样的话我们只需要枚举每个数据,然后打擂台即可。时间复杂度可以从O(n^{3})降到O(n),效率是非常高的。


具体来说就是,我们可以通过遍历整个序列,同时记录当前的最小值和最小值对应的下标。在遍历过程中,每当遇到一个比当前最小值更小的数,就更新最小值和对应下标。遍历完整个序列后,我们就可以确定最小值所在的位置。

接下来,我们从最小值所在的位置开始向左右两侧扩展,直到区间和不再增加为止。在扩展的过程中,我们可以通过前缀和来计算区间和。具体来说,我们可以分别计算最小值左侧和右侧的前缀和,然后不断向两侧扩展,每次将区间和加上左侧或右侧的一个数,直到区间和不再增加为止。

这样,我们就可以在O(n)的时间复杂度内找到最小值和乘积最大的区间。

四.建模

看到上面的分析思路,我们该怎么通过程序来实现呢?这时我们可以想到单调栈

就是我们可以将每一个元素进栈,如果新元素小于栈顶元素,就表示我们要更换最小值了,就可以把栈中大的元素弹出去,同时了结它,计算 值*区间和。但弹出中我们要注意继承它的值给小的元素,比如(2,4,3),3把4弹出的过程中要继承4的值,因为当3作为最小值,区间成员应为(3,4)。关于区间下标的问题,就记录一下就行了,比较好理解,详细下面内容以及代码都有讲解。

五.难点&&问题解答

        1.元素是如何继承的,来保证大元素被弹出栈后,小元素区间中还能有它?

        2.结果中如何确定它的左右区间

答:

在使用单调栈时,我们可以通过维护一个单调递增或单调递减的栈来保证元素的继承。

具体来说,在单调递增栈中,如果当前元素比栈顶元素小,则说明当前元素是栈顶元素右侧的第一个比它小的元素,因此栈顶元素的右侧区间已经确定,可以将栈顶元素弹出栈;在单调递减栈中,如果当前元素比栈顶元素大,则说明当前元素是栈顶元素右侧的第一个比它大的元素,因此可以将当前元素压入栈中。

在弹出栈顶元素时,我们需要记录它的左侧区间和被覆盖的和,以便后续计算区间和的时候使用。在计算被覆盖的和时,我们需要将栈顶元素的左侧区间和被覆盖的和加上它自己的值,以便正确计算被覆盖的和。

通过这种方式,我们可以保证大元素被弹出栈后,小元素区间中还能有它。

六.代码(用于讲解,ac代码见第八点)

#include<bits/stdc++.h>
using namespace std;
int a[1000005];
struct stack {
    int top; //栈顶指针
    int num[1000005]; //数字
    int l[1000005]; //左边界
    int sum[1000005]; //被覆盖的和
} s; //使用结构体模拟栈
int main() {
    int n;
    cin>>n;
    int i,j,ll,k; //循环计数器和临时变量
    s.top=0; //初始化栈顶指针
    int maxn=-1,ansl,ansr; //最大值和对应区间的左右端点
    for(i=1; i<=n; i++) {
        cin>>a[i]; //读入序列
        k=0; //初始化被覆盖的和
        ll=i; //暂定最左端是自己
        
        while(s.top!=0&&s.num[s.top]>=a[i]) { //栈非空且栈顶元素大于等于a[i],出栈
            k+=s.sum[s.top]; //计算被覆盖的和
            ll=s.l[s.top]; //更新左端点
            if(k*s.num[s.top]>maxn) { //更新最大值和对应区间
                maxn=k*s.num[s.top];
                ansl=s.l[s.top];
                ansr=i-1;
            }
            //输出调试信息
            cout<<s.num[s.top]<<" "<<k*s.num[s.top]<<" "<<s.l[s.top]<<" "<<i-1<<endl;
            s.top--; //弹出栈顶元素
        }
        s.num[++s.top]=a[i]; //将a[i]压入栈中
        s.sum[s.top]=k+a[i]; //记录被覆盖的和
        s.l[s.top]=ll; //记录左端点
    }
    k=0; //初始化被覆盖的和
    while(s.top!=0) { //将栈中剩余元素依次弹出
        k+=s.sum[s.top]; //计算被覆盖的和
        if(k*s.num[s.top]>maxn) { //更新最大值和对应区间
            maxn=k*s.num[s.top];
            ansl=s.l[s.top];
            ansr=n;
        }
        //输出调试信息
        cout<<s.num[s.top]<<" "<<k*s.num[s.top]<<" "<<s.l[s.top]<<" "<<n<<endl;
        s.top--; //弹出栈顶元素
    }
    cout<<"--------------"<<endl;
    cout<<maxn<<" "<<ansl<<" "<<ansr; //输出最大值和对应区间
    return 0; 
}

七.代码讲解

        (一)结果-最大值

样例数组:3 1 6 4 5 2

输入样例结果为: 

图解

解析:程序先1弹出了3,即当3为最小值时 ,区间只有【3】,若再往后为【3,1】则最小值就是1了。当1弹出3时,1就要继承3,我们的处理时s.sum[1]=3+1;     //3为继承元素,1是它自己

然后继续入栈,当4入栈是,按照规则,自然要弹出6,则诞生区间【6】;4要继承6的值,即s.sum[4]=4+6;

重点!!!

刚开始我也不能理解,当2给5弹出时,5中序列为【5】,2继承5,为7.

然后再弹出4,但4先给6弹出过,序列自然为【6,4】,但正确应该为【6,4,5】在它前面的5呢?没有继承怎么办,其实在这一点我们要注意这段代码的作用

这里是while,k是累加的,所以这时虽然s.sum[top=4]=10,但这前累加k=5并未清除,再累加k+=10;k=15这时就区间为【4,6,5】

总结:4前面的数是通过sum数组继承的,后面的数是k累加的。

这时不得不提一下单调栈的优势了,因为是递增,一定满足先弹出的值一定在后弹出值的区间内,所以可以k累加实现

最后一步,弹出栈的剩余值

 


        (二) 结果-区间下标

我们先是默认L=i,即自己,若不把后面的元素弹出,则比较简单,直接  s.l[s.top]=ll; //记录左端点

这一步主要为下面做铺垫。

若要把栈顶弹出,则右边界自然是栈顶,即i-1;左边界则是  ansl=s.l[s.top]; //是弹出最后元素的边界值。

比如(3,1,6,4,5,2),现在要找4为最小值的区间。

因为2把5和4弹出的,固r边界即为i-1 = 6-1 = 5,刚好是5的下标

又因为4最后把6弹出,栈顶减到6的top。l边界为L【6】=3;

小结:left与这个数弹掉的元素下标有关;right与把这个数弹掉的元素下标有关。

讲解完毕,如有不懂或错误,欢迎指正! 

八.【AC】代码(含注释)

#include<bits/stdc++.h>
using namespace std;
int a[1000005];
struct stack {
    int top; //栈顶指针
    int num[1000005]; //数字
    int l[1000005]; //左边界
    int sum[1000005]; //被覆盖的和
} s; //使用结构体模拟栈
int main() {
    int n;
    cin>>n;
    int i,j,ll,k; //循环计数器和临时变量
    s.top=0; //初始化栈顶指针
    int maxn=-1,ansl,ansr; //最大值和对应区间的左右端点
    for(i=1; i<=n; i++) {
        cin>>a[i]; //读入序列
        k=0; //初始化被覆盖的和
        ll=i; //暂定最左端是自己
        
        while(s.top!=0&&s.num[s.top]>=a[i]) { //栈非空且栈顶元素大于等于a[i],出栈
            k+=s.sum[s.top]; //计算被覆盖的和
            ll=s.l[s.top]; //更新左端点
            if(k*s.num[s.top]>maxn) { //更新最大值和对应区间
                maxn=k*s.num[s.top];
                ansl=s.l[s.top];
                ansr=i-1;
            }
            //输出调试信息
          //  cout<<s.num[s.top]<<" "<<k*s.num[s.top]<<" "<<s.l[s.top]<<" "<<i-1<<endl;
            s.top--; //弹出栈顶元素
        }
        s.num[++s.top]=a[i]; //将a[i]压入栈中
        s.sum[s.top]=k+a[i]; //记录被覆盖的和
        s.l[s.top]=ll; //记录左端点
    }
    k=0; //初始化被覆盖的和
    while(s.top!=0) { //将栈中剩余元素依次弹出
        k+=s.sum[s.top]; //计算被覆盖的和
        if(k*s.num[s.top]>maxn) { //更新最大值和对应区间
            maxn=k*s.num[s.top];
            ansl=s.l[s.top];
            ansr=n;
        }
        //输出调试信息
       // cout<<s.num[s.top]<<" "<<k*s.num[s.top]<<" "<<s.l[s.top]<<" "<<n<<endl;
        s.top--; //弹出栈顶元素
    }
    //cout<<"--------------"<<endl;
    cout<<maxn<<" "<<ansl<<" "<<ansr; //输出最大值和对应区间
    return 0; 
}

九.总结

单调栈是非常终于的数据结构,值得重视。它一般处理有规律的数据,如:

  1. 求解区间最小值/最大值:单调栈可以用来求解一个序列中每个区间的最小值或最大值,具体做法是维护一个单调递增或单调递减的栈,并在遇到一个比栈顶元素更小或更大的元素时,弹出栈顶元素并更新区间最小值或最大值。

  2. 计算区间和/乘积:单调栈可以用来计算一个序列中每个区间的和或乘积,具体做法是维护一个单调递增或单调递减的栈,并在遇到一个比栈顶元素更小或更大的元素时,弹出栈顶元素并计算被覆盖的区间和或乘积。

  3. 判断是否存在满足条件的区间:单调栈可以用来判断一个序列中是否存在一个区间满足某些特定的条件,例如:区间和大于等于k、区间长度大于等于m等。

总之,单调栈是一种非常实用的数据结构,可以帮助我们高效地解决许多问题。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Python中,单调栈和单调队列是两种不同的数据结构单调栈是一个,它的特点是内的元素是单调的,可以是递增或递减的。在构建单调栈时,元素的插入和弹出都是在的一端进行的。与此类似,单调队列也是一个队列,它的特点是队列内的元素是单调的,可以是递增或递减的。在构建单调队列时,元素的插入是在队列的一端进行的,而弹出则是选择队列头进行的。 单调队列在解决某些问时,能够提升效率。例如,滑动窗口最大值问可以通过使用单调队列来解决。单调队列的结构可以通过以下代码来实现: ```python class MQueue: def __init__(self): self.queue = [] def push(self, value): while self.queue and self.queue[-1 < value: self.queue.pop(-1) self.queue.append(value) def pop(self): if self.queue: return self.queue.pop(0) ``` 上述代码定义了一个名为MQueue的类,它包含一个列表作为队列的存储结构。该类有两个方法,push和pop。push方法用于向队列中插入元素,它会删除队列尾部小于插入元素的所有元素,并将插入元素添加到队列尾部。pop方法用于弹出队列的头部元素。 总结来说,单调栈和单调队列都是为了解决特定问而设计的数据结构单调栈在构建时元素的插入和弹出都是在的一端进行的,而单调队列则是在队列的一端进行的。在Python中,可以通过自定义类来实现单调队列的功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值