单调栈
单调栈:
单调栈解决的是:以某个值为最小(最大)值的最大区间。
实现方法:
求最小值(最大值)的最大区间,维护一个递减(递增)的栈。(下面以求最小值最大区间为例)
当遇到一个比栈顶小的值的时候开始弹栈,弹栈停止的位置到这个值的区间即为此值左边的最大区间;同时,当一个值被弹掉的时候也就意味着比它更小的值来了,也可以计算被弹掉的值的右边的最大区间。
单调递增:数据出栈的序列为单调递增序列(即从栈顶到栈底的元素是单调递增的)
单调递减:数据出栈的序列为单调递减序列(与上面相反)
模板如下:
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\n",Max); } return 0; }
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\n",ans); } return 0; }
题目: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\n",ans); printf("%lld %lld\n",l,r); } return 0; }