单调栈

单调栈

定义

顾名思义,就是栈中存储元素的某种信息是单调的栈。
单调栈可以干什么呢?
可以线性寻找一个元素左边(或右边)第一个满足某种条件的元素。
比较常见的问题是:给定一个序列,对于每个数寻找其左边(或右边)第一个比它大(或比它小)的数。

算法流程

单调栈是怎么实现的呢?我们以寻找每个数右边第一个比它大的数为例。
我们从左往右扫这个序列,维护一个栈,存储当前还没找到比它大的数的元素
可以发现栈中的元素有两个信息是单调的:下标和数值。
下标单调是由于我们是从左往右扫这个序列,所以入栈的元素一定是按照下标单增的;而且栈是 \(LIFO\) 的结构,所以每次出栈的元素的下标一定是栈中最大的,弹出后仍满足下标的单调性。
数值单增是由于我们栈内维护的是未找到比它大的数的元素,所以栈内的元素的数值应该是单减的。如果出现了单增的情况,即当前我们扫到了第 \(j\) 个数,且此时栈中有个数的下标为 \(i\),满足 \(a[i]<a[j](\)别忘了一定满足 \(i<j)\),那么不就说明第 \(i\) 个数 \(a[i]\) 已经找到了它右面第一个比它大的数 \(a[j]\) 了嘛?所以此时 \(i\) 被弹出栈,使得栈中元素保持单减的性质。
到这里整个算法流程就出来了:
我们从左往右扫整个序列,如果栈为空或当前这个数的数值小于等于栈顶元素的数值,那么我们将它压入栈顶;否则一直将栈顶弹出,直到栈为空或者当前这个数的数值小于等于栈顶元素的数值。
举个例子吧:
\(a[i]\) 表示第 \(i\) 个数是几,\(b[i]\) 表示第 \(i\) 数右边第一个比它大的数的下标是几,栈中维护的是数的下标
\(a[i]:3,7,2,1,5\)
我们扫到第 \(1\) 个数 \(3\),发现此时栈为空,入栈。栈中元素:\(1\)
我们扫到第 \(2\) 个数 \(7\),发现此时栈顶元素 \(a[1]=3<7\),说明栈顶元素找到了第一个比它大的数,我们更新 \(b[1]=2\),弹出 \(1\),此时栈为空,那么压入 \(2\)。栈中元素:\(2\)
我们扫到第 \(3\) 个数 \(2\),发现此时栈顶元素 \(a[2]=7>2\),不更新,我们将 \(3\) 压入栈。栈中元素:\(2,3\)
我们扫到第 \(4\) 个数 \(1\),发现此时栈顶元素 \(a[3]=2>1\),不更新,我们将 \(4\) 压入栈。栈中元素:\(2,3,4\)
我们扫到第 \(5\) 个数 \(5\),发现此时栈顶元素 \(a[4]=1<5\),说明栈顶元素找到了第一个比它大的数,我们更新 \(b[4]=5\),弹出 \(4\),继续看栈顶。此时栈顶元素 \(a[3]=2<5\),说明栈顶元素找到了第一个比它大的数,我们更新 \(b[3]=5\),弹出 \(3\),继续看栈顶。此时栈顶元素 \(a[2]=7>5\),不更新,我们将 \(5\) 压入栈。栈中元素:\(2,5\)
我们扫完之后发现栈内还有元素 \(2,5\),说明这几个数它右面没有比它大的数了,我们令 \(b[2]=b[5]=0\),然后将栈清空。
附上简短的代码:

int a[N],b[N],st[N];
void work()
{
	int top=0;
	for(int i=1;i<=n;i++)
	{
		while(top!=0&&a[st[top]]<a[i])  //如果栈不为空且栈顶元素的数值小于当前数,说明栈顶元素找到了右边第一个大于它的数 
		{
			b[st[top]]=i;               
			top--;
		}
		st[++top]=i;                    //入栈 
	}
	while(top!=0)                           //扫完之后栈未空,说明这些数右面没有比它大的数了 
	{
		b[st[top]]=0;                   //索性赋为0吧,具体根据题目定 
		top--; 
	} 
} 

例题

SP1805 HISTOGRA - Largest Rectangle in a Histogram

简化一下题面:
在一条水平线上有 \(n\) 个宽为 \(1\) 的矩形,求包含于这些矩形的最大子矩形面积。
题解
考虑到答案矩形的高度是由它底边范围内最低的矩形的高度所决定。
朴素的做法是预处理区间最小值,然后枚举左右端点然后算面积,时间复杂度 \(O(n^2)\),显然我们是无法接受的。
我们不妨看作是让每个小矩形分别向左和向右扩展,如果遇到高度比它高的就继续扩展,否则就停止扩展。
这样一来,扩展所成的大矩形的高度我们是知道的,就是这个小矩阵的高度,因为其左右扩展的矩形的高度均不小于它的高度
关键就是要求扩展所成的大矩形的长度。我们可以贪心地想到,在一个矩形高度确定的情况下,底边长度越大面积越大,换句话说,就是要一直扩展到无法再扩展才会更优。
那么这个问题就转化成了:给你一个序列,问每个数左右两边第一个比它小的数的距离乘这个数的值最大是多少。
我们可以用单调栈来求。时间复杂度 \(O(n)\)

#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
const int N=1e6;
int n,top;
ll a[N],L[N],R[N],st[N];
ll work()
{
	for(int i=1;i<=n+1;i++)          //找每个数右边第一个比它小的数,这里循环到n+1就能保证每个数都被弹出栈 
	{
		while(top&&a[st[top]]>a[i]) R[st[top--]]=i;
		st[++top]=i;
	}
	for(int i=n;i>=0;i--)            //找每个数左边第一个比它小的数,这里循环到0就能保证每个数都被弹出栈 
	{
		while(top&&a[st[top]]>a[i]) L[st[top--]]=i;
		st[++top]=i;
	}
	ll ans=0;
	for(int i=1;i<=n;i++)            //计算每个小矩形所能扩展的最大矩形 
	    ans=max(ans,(ll)(R[i]-L[i]-1)*a[i]);  
	return ans; 
}
int main()
{
	while(1)
	{
		scanf("%d",&n);
		if(!n) break;
		for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
		printf("%lld\n",work()); 
	}
	return 0;
}

luogu P1950 长方形

简化一下题面:
给出一个 \(n*m\) 的矩形,只包含 \('.'\)\('*'\) 两种符号,问有多少个子矩形内只包含 \('.'\)
题解
为了方便起见,我们将 \('.'\) 记为 \(0\),将 \('*'\) 记为 \(1\)
我们先预处理每个格子最多能向上延伸几个格子。如果遇到 \(1\) 则记为 \(0\)
先贴上神奇的读入函数:

bool read()                            //手写的read,妈妈再也不用担心我读入字符串了 
{
	char ch=getchar();
	while(ch!='.'&&ch!='*') ch=getchar();
	if(ch=='.') return 0;              
	return 1;
}

然后是预处理每个格子最多能向上延伸多少个格子:

for(int i=1;i<=n;i++)
{
	for(int j=1;j<=n;j++)
	{
		map[i][j]=read();           //读入矩形 
		if(map[i][j]) S[i][j]=0;    //如果当前格子是1,则不能向上延伸,记为0 
		else S[i][j]=S[i-1][j]+1;   //否则就接着上一个格子继续延伸 
	}
}

然后我们枚举长方形的底边在哪条边上,这样我们就相当于固定了长方形底边的高度,这样就转化成了上一个题目,只不过上个题是让求最大面积,本题是让求方案数。
我们利用单调栈求出每个点左边第一个小于等于它的数 \(L[i]\) 和右边第一个小于它的数 \(R[i]\),那么被这一列所限制的长方形数为 \((i-L[i])*(R[i]-i)*h_i\),将所有列相加就是以当前行为底边所能构造的长方形数了 。
解释一下为什么是一个小于等于,一个小于
如果出现了相邻的且高度相等的小矩形,那么两个小于等于是会算重了的,而两个小于又会漏情况,所以只能一个小于等于,一个小于,这样才能做到不重不漏。
不重:当且仅当在同一行存在两个数 \(L[i]=L[j]\) 并且 \(R[i]=R[j]\) 的时候,才有可能会算重矩形。但是这种情况是不存在的,因为 \(L[i]\) 满足了左边第一个小于等于的,\(R[i]\) 满足了右边第一个小于的,显然无法构造出这样的情况。
不漏:对于一个矩形,总有一个 \(L[i]\)\(R[i]\) 能框住矩形的两边,故这个矩形一定能被算到。
时间复杂度 \(O(mn)\)

#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
bool read()                 //手写读入顶呱呱                                
{
	char ch=getchar();
	while(ch!='.'&&ch!='*') ch=getchar();
	if(ch=='.') return 0;
	return 1;
}
const int N=1005;
int n,m,top;
ll ans;
int S[N][N],L[N],R[N],st[N];
bool map[N][N];              
ll work(int x)
{
	for(int i=1;i<=m+1;i++) //往右找第一个小于它的数,循环到m+1能保证所有数出栈 
	{
		while(top&&S[x][i]<S[x][st[top]]) R[st[top--]]=i;
		st[++top]=i;
	}
	for(int i=m;i>=0;i--)   //往左找第一个小于它的数,循环到0能保证所有数出栈 
	{
		while(top&&S[x][i]<=S[x][st[top]]) L[st[top--]]=i;
		st[++top]=i;
	}
	ll cnt=0;
	for(int i=1;i<=m;i++)   //把被每个小矩形所限制的长方形的构造方案数相加,就是被这一行所限制的长方形构造方案数 
	    cnt+=(i-L[i])*(R[i]-i)*S[x][i];
    return cnt;
}
int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			map[i][j]=read();
			if(map[i][j]) S[i][j]=0;    //预处理出每个格子连续向上为0的最长长度,也就是小矩形的高度 
			else S[i][j]=S[i-1][j]+1;
		}
	}
	for(int i=1;i<=n;i++)   //枚举每一行,算被每一行所限制的长方形的构造方案数     
	    ans+=work(i);
	printf("%lld\n",ans);
	return 0;
}
posted @ 2020-08-05 15:08  暗い之殇  阅读(462)  评论(0编辑  收藏  举报