单调栈算法总结&专题训练

单调栈算法总结&专题训练

1.概述

单调栈,是一种数据结构,与单调队列相似。

单调队列使用双端队列维护,队列内元素单调递增或单调递减。

单调栈则使用普通的栈维护,栈内元素单调递增或单调递减。

接下来,通过一道例题,来看一下单调栈的基本操作。

2.模板

link

作为模板题,我将会详细讲解单调栈的用法。

单调栈其实类似于单调队列(不了解的可以看一看这篇文章),只不过在维护时不需要考虑元素过时问题。

通常,单调栈分为两种:单调递增栈与单调递减栈。

  • 单调递增栈:栈内元素单调递增。(如 1 2 3
  • 单调递减栈:栈内元素单调递减。(如 3 2 1

接下来通过样例,详细说明单调栈的维护过程。

5
1 4 2 3 5

首先,类比单调队列的思想,要求大于 a i a_i ai 的第一个数,那么应该维护一个 单调递减栈 。为什么不用单调递增栈呢?原因见下文。

接下来我们规定 f f f 为集合 A = { f i ∣ i ∈ [ 1 , n ] } A=\{f_i|i \in [1,n]\} A={ fii[1,n]}

第一个数: a 1 = 1 a_1=1 a1=1 ,加入栈中。

栈:1 f = { 0 , 0 , 0 , 0 , 0 } f=\{0,0,0,0,0\} f={ 0,0,0,0,0}

第二个数: a 2 = 4 > a 1 a_2=4>a_1 a2=4>a1 ,考虑要维护单调递减栈,于是我们弹出 a 1 a_1 a1 ,同时记录 f 1 = 2 f_1=2 f1=2 (因为无论后面的数有多大,都不能再影响 a 1 a_1 a1 了),加入 a 2 a_2 a2

栈: 2 f = { 2 , 0 , 0 , 0 , 0 } f=\{2,0,0,0,0\} f={ 2,0,0,0,0}

第三个数: a 3 = 2 a_3=2 a3=2 ,考虑要维护单调递减栈,加入 a 3 a_3 a3

栈: 4 2 f = { 2 , 0 , 0 , 0 , 0 } f=\{2,0,0,0,0\} f={ 2,0,0,0,0}

第四个数: a 4 = 3 a_4=3 a4=3 ,弹出 2 ,加入 a 4 a_4 a4 。更新 f 3 = 4 f_3=4 f3=4

为了更新答案方便,在程序中我依然在栈中存放下标。这里使用原数。

栈: 4 3 f = { 2 , 0 , 4 , 0 , 0 } f=\{2,0,4,0,0\} f={ 2,0,4,0,0}

第五个数: a 5 = 5 a_5=5 a5=5 ,弹出所有数,加入 a 5 a_5 a5 ,更新 f 2 = 5 , f 4 = 5 f_2=5,f_4=5 f2=5,f4=5

栈: 5 f = { 2 , 5 , 4 , 5 , 0 } f=\{2,5,4,5,0\} f={ 2,5,4,5,0}

完结撒花~~~

这里说明一下为什么不用单调递增栈:

如果手动模拟一遍,会发现在处理 a 5 a_5 a5 时,栈内元素为 1 , 2 , 3 1,2,3 1,2,3 (如果能够模拟出来说明已经掌握),加入 a 5 a_5 a5 时, f 4 f_4 f4 是被更新了,但是 f 2 f_2 f2 不能被更新(相反的, f 2 = 3 f_2=3 f2=3 ),所以不能使用单调递增栈。

特别说明一下:针对同样的元素,一般的题目单调栈内是可以维护的(也就是都进栈),不会对答案产生影响。

接下来是代码。请在确保看懂上述过程后再看代码。

#include<bits/stdc++.h>
using namespace std;

const int MAXN=3e6+10;
int a[MAXN],n,f[MAXN],p,sta[MAXN];

int read()
{
    
    
	int sum=0,fh=1;char ch=getchar();
	while(ch<'0'||ch>'9') {
    
    if(ch=='-') fh=-1;ch=getchar();}
	while(ch>='0'&&ch<='9') {
    
    sum=(sum<<3)+(sum<<1)+ch-'0';ch=getchar();}
	return sum*fh;
}

int main()
{
    
    
	n=read();p=0;//使用数组模拟栈
	for(int i=1;i<=n;i++)
	{
    
    
		a[i]=read();
		if(p==0) sta[++p]=i;//处理空栈
		else
		{
    
    
			while(p!=0&&a[sta[p]]<a[i]) f[sta[p--]]=i;//更新答案,不要忘记判定空栈
			sta[++p]=i;
		}
	}
	for(int i=1;i<=n;i++) cout<<f[i]<<" ";
	cout<<"\n";
	return 0;
}

如果你成功看懂了上述代码,那么恭喜你,学会了单调栈!

接下来是几道例题。

3.例题

题单:

  1. link
  2. link
  3. link
  4. link
  5. link

T1:

简直就是裸题,类比例题正着跑一遍单调递减栈,反着跑一遍单调递减栈即可。

当然:

道路千万条,long long 第一条。
结果存 int ,爆零两行泪。

代码:

#include<bits/stdc++.h>
using namespace std;

const int MAXN=1e6+10;
int n,h[MAXN],v[MAXN],faft[MAXN],fpre[MAXN],p,sta[MAXN];
typedef long long LL;
LL sum[MAXN],ans;

int read()
{
    
    
	int sum=0,fh=1;char ch=getchar();
	while(ch<'0'||ch>'9') {
    
    if(ch=='-') fh=-1;ch=getchar();}
	while(ch>='0'&&ch<='9') {
    
    sum=(sum<<3)+(sum<<1)+ch-'0';ch=getchar();}
	return sum*fh;
}

int main()
{
    
    
	n=read();
	for(int i=1;i<=n;i++) {
    
    h[i]=read();v[i]=read();}
	p=0;
	for(int i=1;i<=n;i++)
	{
    
    
		if(p==0) sta[++p]=i;
		else
		{
    
    
			while(h[sta[p]]<h[i]&&p!=0) faft[sta[p--]]=i;
			sta[++p]=i;
		}
	}//正跑单调栈
	memset(sta,0,sizeof(sta));
	p=0;
	for(int i=n;i>=1;i--)
	{
    
    
		if(p==0) sta[++p]=i;
		else
		{
    
    
			while(h[sta[p]]<h[i]&&p!=0) fpre[sta[p--]]=i;
			sta[++p]=i;
		}
	}//反跑单调栈
	for(int i=1;i<=n;i++)
	{
    
    
		sum[fpre[i]]+=(LL)v[i];
		sum[faft[i]]+=(LL)v[i];
	}//统计答案
	for(int i=1;i<=n;i++) ans=max(ans,sum[i]);
	cout<<ans<<"\n";
	return 0;
}

T2:

这道题是道好题目,考验了对于单调栈内元素单调性的应用。

首先,我们不难想到,要去维护一个单调递减栈 (单调递增栈:为什么我还不能上场) 。为什么?因为如果一个人碰到了比自己高的人,他就不会再对答案做出贡献了,而处理第一个比自己高的人不正好可以使用单调递减栈吗?

然后,我们发现。。。。。统计答案就出了问题:因为栈内我们维护了同样的元素,所以如果要暴力去求答案,时间复杂度就会升至 O ( N 2 ) O(N^2) O(N2) ,那么妥妥的 TLE 。

因此,这里就要利用好单调栈的单调性了。

我们在加入 a i a_i ai 新数时,统计答案是到第一个小于等于 a i a_i ai 的数(注意:等于也是可以的)。第一个小于等于 a i a_i ai 的数?单调栈又有单调性???所以?????可以使用二分进行优化!这样,时间复杂度完美的降至 O ( N log ⁡ N ) O(N\log N) O(NlogN),可以顺利通过本题。

代码:

#include<bits/stdc++.h>
using namespace std;

const int MAXN=5e5+10;
typedef long long LL;
LL ans;
int n,a[MAXN],p,sta[MAXN];

int read()
{
    
    
	int sum=0,fh=1;char ch=getchar();
	while(ch<'0'||ch>'9') {
    
    if(ch=='-') fh=-1;ch=getchar();}
	while(ch>='0'&&ch<='9') {
    
    sum=(sum<<3)+(sum<<1)+ch-'0';ch=getchar();}
	return sum*fh;
}

int main()
{
    
    
	n=read();
	for(int i=1;i<=n;++i) a[i]=read();
	for(int i=1;i<=n;++i)
	{
    
    
		if(p==0) sta[++p]=a[i];
		else if(sta[p]>a[i]) sta[++p]=a[i],ans++;
		else
		{
    
    
			int l=1,r=p,t=0;
			while(l<r)
			{
    
    
				int mid=(l+r)>>1;
				if(r==l+1) mid=r;
				if(a[i]>=sta[mid]) r=mid-1;
				else l=mid;
			}
			ans+=(LL)p-l+1;
			while(p!=0&&sta[p]<a[i]) p--;
			sta[++p]=a[i];
		}
	}
	cout<<ans<<"\n";
	return 0;
}

T3:

这道题也非常经典,在很多地方也都是被当作例题讲解的。

由于矩形只能向左或向右扩展,为了处理方便,我们考虑向左扩展。

显然的,向左扩展时只有碰到比自己低的才会降低高度,而此时单调递减栈就不能用了,必须使用单调递增栈维护 (单调递增栈:终于想起我了)

然后考虑更新答案。显然的,维护的时候我们边弹出元素边更新答案。在更新答案时,我们累计当前的宽度(注意不能直接拿下标相减求得宽度,因为里面有些点高度特别小,这些点可能会影响答案),用当前向左扩展所累积的宽度乘以当前栈顶元素就是矩形面积,然后求最大值即可。

如果不理解,可以手造几组样例模拟一下。

为了处理方便,我们在首尾分别插入一个 0。

首:没有什么作用,只是保证栈不为空。

尾:作用很大!在程序结束时可能会有几个数据没有弹出栈,影响最后的答案,而加入 0 之后,由于是单调递增栈,所以最后一个 0 可以一次性弹出所有元素(当然它自己和一开始的 0 不能弹出),保证答案的正确性。

代码:

#include<bits/stdc++.h>
using namespace std;

const int MAXN=1e5+10;
int n,h[MAXN],p,sta[MAXN],wid[MAXN];
typedef long long LL;
LL ans;

int read()
{
    
    
	int sum=0,fh=1;char ch=getchar();
	while(ch<'0'||ch>'9') {
    
    if(ch=='-') fh=-1;ch=getchar();}
	while(ch>='0'&&ch<='9') {
    
    sum=(sum<<3)+(sum<<1)+ch-'0';ch=getchar();}
	if(fh==1) return sum;
	return -sum;
}

int main()
{
    
    
	while(1)
	{
    
    
		n=read();p=0;h[n+1]=0;ans=0;sta[++p]=0;wid[p]=1;
		if(n==0) break;
		for(int i=1;i<=n;i++) h[i]=read();
		for(int i=1;i<=n+1;i++)
		{
    
    
				int len=0;
				while(p!=0&&h[sta[p]]>h[i])
				{
    
    
					len+=wid[p];
					ans=max(ans,(LL)h[sta[p]]*len);
					p--;
				}
				sta[++p]=i;
				wid[p]=len+1;
		}
		cout<<ans<<"\n";
	}
	return 0;
}

T4:

详见这篇文章:link

T5:

前置题单的代码就不贴了。

前置题单T1:

简单 dp ,设 f i , j f_{i,j} fi,j 表示从 ( i , j ) (i,j) (i,j) (表示第 i i i 行第 j j j列,下同)到 ( 1 , 1 ) (1,1) (1,1) 最大正方形的边长,易得 f i , j = min ⁡ { f i − 1 , j , f i , j − 1 , f i − 1 , j − 1 } + 1 f_{i,j}=\min\{f_{i-1,j},f_{i,j-1},f_{i-1,j-1}\}+1 fi,j=min{ fi1,j,fi,j1,fi1,j1}+1,直接递推即可。

前置题单T2:

方程一样,只需要提前对输入的数据做处理,然后跑一遍全是 1 的最大正方形,全是 0 的最大正方形即可。

这里说一下怎么做处理:考虑到要求黑白相间,那么我们针对 i ∗ j m o d    2 = 1 i*j\mod2=1 ijmod2=1 的点取反即可。

取反的代码:

for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			a[i][j]=read()^((i^j)&1);

其中, i ⊕ j & 1 i \oplus j \& 1 ij&1 可以处理出当前格子是否需要取反,然后利用异或的特性就可以成功取反,然后,地图就变成了前置题单 T1 的样子。

前置题单T3:

f i , j f_{i,j} fi,j 表示最大土地面积。为了做题方便,我们规定任意矩形一旦定下一条边之后,就只能向上面扩展,去除后效性。

这道题越看越像最大子矩阵问题,但是我们需要处理的是面积。

为了得到最大的面积,我们首先可以先对 ( i , j ) (i,j) (i,j) 做一个处理,预处理出 ( i , j ) (i,j) (i,j) 能够向上扩展的最大长度,存在 g i , j g_{i,j} gi,j 当中。

然后,针对这一类矩阵内求某某最大值的问题,有一种思路就是枚举下边界,在本题中就是先枚举矩形下面一条边所在的位置。

然后呢?由于 ( i , j ) (i,j) (i,j) 只能向上扩展 g i , j g_{i,j} gi,j 格,那么这道题不就变成了 T3 了吗?模拟 T3 跑一遍就可以了。

时间复杂度:枚举下边界 O ( n ) O(n) O(n) ,单调栈时间复杂度 O ( n ) O(n) O(n) ,总时间复杂度 O ( n 2 ) O(n^2) O(n2)

现在再回到这道题,前置题单全部搞定之后,这道题就是一道裸题了!!!首先,类比 前置题单T2 对地图进行一遍处理,然后跑一遍 前置题单T2 ,再跑一遍 前置题单T3 ,不就做完了?而 前置题单T1 是为 前置题单T2 做铺垫的。

代码(这里借鉴了本机房 jxw 大佬的思路与码风,表示感谢):

#include<bits/stdc++.h>

const int MAXN=2e3+10;
int n,m,a[MAXN][MAXN];

int read()
{
    
    
	int sum=0,fh=1;char ch=getchar();
	while(ch<'0'||ch>'9') {
    
    if(ch=='-') fh=-1;ch=getchar();}
	while(ch>='0'&&ch<='9') {
    
    sum=(sum<<3)+(sum<<1)+ch-'0';ch=getchar();}
	return sum*fh;
}

namespace zfx
{
    
    
	int f[MAXN][MAXN],ans=0;
	int solve(int op)
	{
    
    
		memset(f,0,sizeof(f));
		for(int i=1;i<=n;i++)
			for(int j=1;j<=m;j++)
				if(a[i][j]==op) f[i][j]=std::min(std::min(f[i-1][j],f[i][j-1]),f[i-1][j-1])+1;
		for(int i=1;i<=n;i++)
			for(int j=1;j<=m;j++)
				ans=std::max(ans,f[i][j]);
		return ans*ans;
	}
}
namespace cfx
{
    
    
	int g[MAXN][MAXN],p,sta[MAXN],wid[MAXN],ans=0;
	int solve(int op)
	{
    
    
//		std::cout<<op<<"\n";
		memset(g,0,sizeof(g));
		for(int i=1;i<=n;i++)
			for(int j=1;j<=m;j++)
				if(a[i][j]==op) g[i][j]=g[i-1][j]+1;
//		for(int i=1;i<=n;i++)
//		{
    
    
//			for(int j=1;j<=m;j++) std::cout<<g[i][j]<<" ";
//			std::cout<<"\n";
//		}
		for(int i=1;i<=n;i++)
		{
    
    
			memset(sta,0,sizeof(sta));
			memset(wid,0,sizeof(wid));
			p=0;sta[++p]=0;wid[p]=1;
			for(int j=1;j<=m+1;j++)
			{
    
    
				int len=0;
				while(p!=0&&g[i][sta[p]]>g[i][j])
				{
    
    
					len+=wid[p];
					ans=std::max(ans,g[i][sta[p]]*len);
					p--;
				}
				sta[++p]=j;
				wid[p]=len+1;
			}
		}
		return ans;
	}
}

int main()
{
    
    
	n=read();m=read();
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			a[i][j]=read()^((i^j)&1);
//	for(int i=1;i<=n;i++)
//	{
    
    
//		for(int j=1;j<=m;j++) std::cout<<a[i][j]<<"\n";
//		std::cout<<"\n";
//	}
	std::cout<<std::max(zfx::solve(1),zfx::solve(0))<<"\n";
	std::cout<<std::max(cfx::solve(1),cfx::solve(0))<<"\n";
	return 0;
}

4.总结

单调栈其实就是弱化版的单调队列,码量与常数都比单调队列小(其实个人感觉手打队列/栈时常数没有什么区别),也比较方便,但是如果数列中的元素有 寿命长短 区间限制那么就需要使用单调队列求解。也可以说,单调队列就是扩展的单调栈,它们的关系就跟线段树与树状数组一样。

猜你喜欢

转载自blog.csdn.net/BWzhuzehao/article/details/109822425