【单调栈】
基本过程:
让我们来模拟一个递增的单调栈的实现过程,以序列{7, 2, 5, 3, 11, 9}为例。
主要步骤如下:若栈为空或者栈顶元素小于当前元素则压入,否则弹出栈内比当前元素大的所有元素。
第一步:栈为空,压入7。此时栈内:7 。
第二步:7比2大,弹出7,压入2。此时栈内:2 。
第三步:2比5小,压入5。此时栈内:2 5 。
第四步:5比3大,弹出5,2比3小,压入3。此时栈内:2 3 。
第五步:3比11小,压入11 。此时栈内:2 3 11 。
第六步:11比9大,弹出11,3比9小,压入9 。此时栈内:2 3 9 。
可以在纸上模拟一下这个过程。在这个过程中我们可以跑出以每个元素作为最小值的最左端点下标。
【实现代码】
#include <bits/stdc++.h>
using namespace std;
int main()
{
int a[]={0,7,2,5,3,11,9};
int l[10];
stack <int> stk;
for(int i=1;i<=6;i++){
while(!stk.empty()&&a[stk.top()]>=a[i]) stk.pop();
if(stk.empty()) l[i]=1;
else l[i]=stk.top()+1;
stk.push(i);
}
for(int i=1;i<=6;i++)
printf(i<6?"%d ":"%d\n",l[i]);
}
跑出来的结果是1,1,3,3,5,5
同理,我们可以跑出每个元素作为最小值的最右端点的下标,只要从右往左跑就可以了。还有用递减的单调栈跑出每个元素作为最大值的区间的下标。可以自己尝试写一下。时间复杂度为O(n).
【单调栈基础应用】
1.给定一组数,跑出每个数和他右边第一个比他大(小)的数之间有多少个数。
2.给定一序列,寻找某一子序列使得子序列中的最小值乘以子序列的长度最长。
3.给定一序列,寻找某一子序列使得子序列中的最小值乘以子序列所有元素和最大。
【例题讲解】
EX1:POJ3250
【题解】
题意:每头牛面朝右边,只能看到比他矮的牛,询问所有的牛能看到的牛的数目的期望。
思路:从前往后跑,每次弹出小于等于当前身高的元素,因为如果不高于当前身高就没办法越过这个位置看到后面的牛。然后每次加上当前栈内元素个数,即能看到这个位置的前面的牛的数目即可。
【代码】
#include <cstdio>
#include <stack>
using namespace std;
#define ll long long
int main()
{
int n;
while(~scanf("%d",&n)){
ll sum=0;
stack <int> stk;
for(int i=0;i<n;i++){
int x; scanf("%d",&x);
while(!stk.empty()&&stk.top()<=x) stk.pop();
sum+=stk.size();
stk.push(x);
}
printf("%lld\n",sum);
}
}
EX2:POJ2559
Largest Rectangle in a Histogram
【题解】
题意:给定一个直方图,每一个矩形宽度为1,第i个矩形高度为hi,问这样一个直方图形成的最大矩形面积是多少?
思路:跑出以每个矩形高度为最小值的左右区间,区间长度即为固定高度的最大宽度。更新答案即可。
【代码】
#include <cstdio>
#include <stack>
using namespace std;
#define ll long long
const int maxn=1e5+10;
int l[maxn],r[maxn];
ll a[maxn];
int main()
{
int n;
while(~scanf("%d",&n)&&n){
stack <int> stk;
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
while(!stk.empty()&&a[stk.top()]>=a[i]) stk.pop();
if(stk.empty()) l[i]=1;
else l[i]=stk.top()+1;
stk.push(i);
}
while(!stk.empty()) stk.pop();
for(int i=n;i>0;i--){
while(!stk.empty()&&a[stk.top()]>=a[i]) stk.pop();
if(stk.empty()) r[i]=n;
else r[i]=stk.top()-1;
stk.push(i);
}
ll ans=0;
for(int i=1;i<=n;i++)
ans=max(ans,a[i]*(r[i]-l[i]+1));
printf("%lld\n",ans);
}
}
EX3:POJ2796
【题解】
题意:给定一个长度为n的序列,输出区间最小值乘区间和最大的一个区间的值和左右端点。
思路:跑出以每个位置为最小值的左右区间,用前缀和维护区间和,更新最大答案即可。
【代码】
#include <cstdio>
#include <stack>
using namespace std;
#define ll long long
const int maxn=1e5+10;
ll a[maxn],sum[maxn];
int l[maxn],r[maxn];
int main()
{
int n;
while(~scanf("%d",&n)){
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]),sum[i]=sum[i-1]+a[i];
stack <int> stk;
for(int i=1;i<=n;i++){
while(!stk.empty()&&a[stk.top()]>=a[i]) stk.pop();
if(stk.empty()) l[i]=1;
else l[i]=stk.top()+1;
stk.push(i);
}
while(!stk.empty()) stk.pop();
for(int i=n;i>=1;i--){
while(!stk.empty()&&a[stk.top()]>=a[i]) stk.pop();
if(stk.empty()) r[i]=n;
else r[i]=stk.top()-1;
stk.push(i);
}
ll ans=-1,ret,index;
for(int i=1;i<=n;i++){
ret=a[i]*(sum[r[i]]-sum[l[i]-1]);
if(ans<ret){
index=i;
ans=ret;
}
}
printf("%lld\n%d %d\n",ans,l[index],r[index]);
}
return 0;
}