单调栈的应用
在刚接触单调栈的时候,我们在做了几道入门题后可能会对单调栈有一定的了解,但是却有点难以归纳说出单调栈到底适用于什么题目、能解决什么样的问题(或者在遇到什么类型的题目时我们要想到用单调栈),我刚开始也百思不得其解,但在后面将几道相关题目一一对比之后,我忽然对单调栈的应用有了更深的理解。
1.单调栈的特点:
1.后进先出
1.单调性:单调栈中存储的所有元素一定是单调增或减的。
2.单调栈的作用归纳:
1.在一个序列中,求最长上升子序列(例1)
2.在一个序列中,求以某一个元素为最小值的最大前驱或后驱上升子序列长度(区间只包含该元素及其右边或左边比他大的元素)(例2)
3.在一个序列中,求以某一个元素为最小值的最大连续区间长度。(区间长度包含该元素及其向左右两边延伸的长度)(例3、例4)
2.例题:
1. LIS(最长上升子序列)
运用单调栈的单调性质,存储序列中的最长上升子序列。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXX=100010;
int a[MAXX],stack[MAXX];
int top,n;
int main(){
scanf("%d",&n);
for(int i=1;i<=n;++i)scanf("%d",&a[i]);
for(int i=1;i<=n;++i){
if(a[i]>stack[top])stack[++top]=a[i];
else {
int pos=lower_bound(stack+1,stack+top+1,a[i])-stack;
stack[pos]=a[i];
}
}
cout<<top;
return 0;
}
2.Bad Hair Day
这题实质上是让你求以每个元素为最小值的前驱区间(不包括该元素)长度之和,如样例 10 3 7 4 12 2,单调栈中的状态依次为 10 -> 10 3 -> 10 7 -> 10 7 4 -> 12 -> 12 2. 以10为最小值的前驱长度(不包括10)为0, 3为 1, 7 为 1, 4 为 2, 12 为 0 , 2 为 1, 加起来的总和为0 + 1 + 1 + 2 + 0 + 1 = 5;
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define IOS std::ios::sync_with_stdio(false);cin.tie(NULL);
#include<stack>
#define int long long
using namespace std;
const int maxn = 1e6 + 10;
stack<int> stk;
void solve(){
int n;
cin >> n;
int sum = 0;
for(int i = 1; i <= n; i++){
int x;
cin >> x;
while(stk.size() && x >= stk.top()){ // 弹出前面比该数小的元素
stk.pop();
}
sum += stk.size();//剩下的就是它前面比他大的元素中, 也就是以它为最小值的最长前驱区间长度
stk.push(x);
}
cout << sum << endl;
}
signed main(){
IOS;
int T;
// scanf("%lld", &T);
T = 1;
while(T--){
solve();
}
}
3.Largest Rectangle in a Histogram
这题就是求出在序列中以每个元素为最小值的最长区间长度,再乘上自己的高度, 计算序列中这样的最大值.所以需要以该元素为中心向其左右延伸,找出区间左限和右限, 以向左寻找为例,利用单调栈, 一直弹出栈中比他大的元素, 直至遇到比他小的元素为止, 这个元素的下标就是它的区间左限. 向右寻找类似
#include <stdio.h>
#include <stack>
#include <algorithm>
using namespace std;
long long int h[100005];
long long int l[100005];
long long int r[100005];
int main()
{
int n;
stack<int> s;
while(~scanf("%d",&n))
{
if(n==0) return 0;
for(int i=1;i<=n;i++)
scanf("%lld",&h[i]);
while(s.size()) s.pop();
for(int i=1;i<=n;i++)
{
while(!s.empty()&&h[s.top()]>=h[i]) //弹出栈中比它大的元素,直到遇到比他小的元素
s.pop();
if(s.empty()) l[i]=0; // 如果栈为空, 它的左限就是0
else l[i]=s.top(); // 如果栈不空,栈顶元素下标(它前面第一个比他小的元素位置)就是它的左限
s.push(i);
}
while(s.size()) s.pop();
for(int i=n;i>=1;i--)
{
while(!s.empty()&&h[s.top()]>=h[i])//与上面一样,只是找右限而已
s.pop();
if(s.empty()) r[i]=n+1;
else r[i]=s.top();
s.push(i);
}
long long int ans=-1;
for(int i=1;i<=n;i++)
ans=max(ans,h[i]*(r[i]-l[i]-1));//高度*区间长度
printf("%lld\n",ans);
}
return 0;
}
4.Largest Submatrix of All 1’s
这题要绕个弯子, 需要预处理矩阵如下
0 1 0 0 0 3 0 0
1 1 1 0 1 2 3 0
0 1 1 0 0 1 2 0
0 0 1 0 0 0 1 0
然后一行行来寻找以该行每列上的数值为最小值的连续区间长度如 第2行的2, 以它为最小值的连续区间为2 3, 长度为2, 所以矩阵的1数位2 * 2 = 4
这里寻找的方法与第3题不同,效率应该会跟快,因为寻找区间左限和右限同时进行。左限寻找前默认设置为自己下标,然后在弹出比他大的元素时,继承该栈顶元素的左限,而自己的位置就是该栈顶元素的右限。
#include<iostream>
#include<cstring>
using namespace std;
const int N=2005;
int a[N][N];
int l[N],s[N];
int main()
{
int n,m;
while(~scanf("%d%d",&m,&n))
{
memset(a,0,sizeof(a));
//读入
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
scanf("%d",&a[i][j]);
//预处理
for(int i=1;i<=n;i++)
for(int j=m;j>=1;j--)
if(a[j][i]) a[j][i]+=a[j+1][i];
int ans=0;
for(int i=1;i<=m;i++)//考虑第i行
{
a[i][n+1]=-1;//让该行最后一个元素得以出栈计算它的区间长度
int top=0;
//维护一个递增栈
for(int j=1;j<=n+1;j++)
{
l[j]=j;//默认该元素区间左限为自己的位置
while(top>0&&a[i][s[top]]>a[i][j])// 把比他大的元素弹出, 直至遇到比他小的元素
{
ans=max(ans,a[i][s[top]]*(j-l[s[top]]));//目前的j就是要弹出元素的区间右限
l[j]=l[s[top]];//即将放入的元素a[i][j]的左限就要继承弹出元素的左限, 因为弹出元素的左区间都大于a[i][j], 是以a[i][j]为最小值的前驱区间
top--;
}
if(top>0&&a[i][s[top]]==a[i][j]) l[j]=l[s[top]];//如果栈顶元素与a[i][j]相等, 那么a[i][j]的左限就能继承栈顶元素的左限
s[++top]=j;
}
}
printf("%d\n",ans);
}
return 0;
}