单调栈:
具有单调性的栈,分为单调递增栈和单调递减栈,这里的顺序是从栈顶到栈底。
单调栈有什么作用呢,为什么要用到单调栈?单调栈的强大是能在O(n)的时间内求出一个数组中所有a[i]的右边(左边)的第一个比它大(小)的数。而普通方法肯定是n2的两个for循环解决,所以他将时间复杂度从O(n2)优化到了O(n)。
接下来说说它的实现:
单调递增栈:现在有一组数10,3,7,4,12。从左到右依次入栈,则如果栈为空或入栈元素值小于栈顶元素值,则入栈;否则,如果入栈则会破坏栈的单调性,则需要把比入栈元素小的元素全部出栈(我们可以看出当前入栈元素就是所有出栈元素的右边第一个比他大的元素) 。接下来用一个最简单的实例理解它的原理。
给出项数为 n 的整数数列 a1…an。让你输出数列中第 i个元素之后第一个大于 ai 的元素的下标,若不存在输出0。
题目链接:https://www.luogu.com.cn/problem/P5788
AC代码:
#include<iostream>
#include<cstdio>
using namespace std;
int n;
int a[3000010],st[3000010],ans[3000010];
int main(){
cin>>n;
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
int l=1,r=0;//这里是手写栈的栈底l和栈顶r。
for(int i=1;i<=n;i++){
while(l<=r&&a[st[r]]<a[i]){//当栈非空且栈顶元素小于当前元素(不满足单调性)需要弹出栈顶元素直至满足单调性。
ans[st[r]]=i;//所有弹出的元素的右边第一个大于自己的元素的下标就是i
r--;
}
st[++r]=i;//将a[i]按进栈。
}
//最后栈中可能还有元素,表明这些元素右边没有比他大的,所以在这道题中可以不管,初始化是0.
for(int i=1;i<=n;i++)cout<<ans[i]<<' ';cout<<endl;
return 0;
}
第二道题:
给一个直方图,求直方图中的最大矩形的面积。例如,下面这个图片中直方图的高度从左到右分别是2, 1, 4, 5, 1, 3, 3, 他们的宽都是1,其中最大的矩形是阴影部分。
Input
输入包含多组数据。每组数据用一个整数n来表示直方图中小矩形的个数,你可以假定1 <= n <= 100000. 然后接下来n个整数h1, …, hn, 满足 0 <= hi <= 1000000000. 这些数字表示直方图中从左到右每个小矩形的高度,每个小矩形的宽度为1。 测试数据以0结尾。
Output
对于每组测试数据输出一行一个整数表示答案。
Sample Input
7 2 1 4 5 1 3 3
4 1000 1000 1000 1000
0
Sample Output
8
4000
分析:这道题我们需要求出对于每个a[i]求出它向左向右能扩展的最大区间(即求出右边第一个比他小的元素的坐标-1;求出左边第一个比他小的元素的坐标+1),然后求出最大面积。对于求左边第一个比他小的元素的坐标+1可以将数组倒序之后求,就是需要转换转换。
#include<iostream>
#include<cstdio>
#include<stack>
using namespace std;
int n;
int a[100010],aa[100010];
int l[100010],r[100010],rr[100010];
stack<int>s;
void solve1(){
for(int i=1;i<=n;i++){
while(!s.empty() &&a[s.top()] >a[i]){
r[s.top()]=i-1;
s.pop() ;
}
s.push(i);
}
while(!s.empty() ){
r[s.top() ]=n;
s.pop() ;
}
}
void solve2(){
for(int i=1;i<=n;i++)aa[i]=a[n-i+1];//顺序颠倒一下
for(int i=1;i<=n;i++){
while(!s.empty() &&aa[s.top()] >aa[i]){
rr[s.top()]=i-1;
s.pop() ;
}
s.push(i);
}
while(!s.empty() ){
rr[s.top() ]=n;
s.pop() ;
}
for(int i=1;i<=n;i++)rr[i]=n-rr[i]+1;//下标的值颠倒回来
for(int i=1;i<=n;i++)l[i]=rr[n-i+1];//顺序颠倒回来
}
int main(){
while(cin>>n){
if(n==0)break;
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
solve1();//求右边第一个比他小的元素的下标-1
solve2();//求左边第一个比他小的元素的下标+1
long long ans=0;
for(int i=1;i<=n;i++){
ans=max(ans,(long long)a[i]*(long long)(r[i]-l[i]+1));
}
cout<<ans<<endl;
}
return 0;
}
其实只需要一次从左到右的遍历也能求出左边第一个比他小的元素的下标。
int l=1,r=0;
for(int i=1;i<=n;i++){
while(l<=r&&a[st[r]]>a[i]){
ans[st[r]]=i-1;
r--;
}
if(a[i]==a[st[r]])ansl[i]=ansl[st[r]];//只需要在这里添加这两句话.
else ansl[i]=st[r]+1;//
st[++r]=i;
}