单调栈的应用

在刚接触单调栈的时候,我们在做了几道入门题后可能会对单调栈有一定的了解,但是却有点难以归纳说出单调栈到底适用于什么题目、能解决什么样的问题(或者在遇到什么类型的题目时我们要想到用单调栈),我刚开始也百思不得其解,但在后面将几道相关题目一一对比之后,我忽然对单调栈的应用有了更深的理解。

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;
}