单调栈的应用总结(附典型例题讲解)

概念引入

单调栈是栈内元素自栈顶到栈底满足单调性的栈,元素在入栈时遵循单调原则,可以在线性时间内求出一个序列中的任一个元素向左(右)所能扩展到的最大长度,也即寻找其左边(右)第一个比它大(小)的数。

作用

  • 单调栈可以求得以当前元素为最值的最大区间
  • 单调递增栈可以找到往左/右第一个比当前元素大的元素
  • 单调递减栈可以找到往左/右第一个比当前元素小的元素

接下来以几个典型题目为例,逐层深入地讲解一下单调栈在一些算法题目中的应用

例题讲解

在看这些题目之前先尝试理解这样一段话,对于单调递减栈来说,一个元素向左遍历的第一个比它小的数就是把它压入单调栈时栈顶的元素(若栈为空则说明在该元素左边不存在比它小的数),一个元素向右遍历的第一个比它小的数就是使该元素出栈的元素,单调递增栈同理, 如果此时还不能理解没关系,看完下面几道题目,相信你一定会对这段话印象深刻。


POJ3250 Bad Hair Day
题目链接

题意
N N N 头牛从左到右一字排开,一头牛可以看到另一头牛当且仅当这头牛的高度大于它和另一头牛之间的所有牛的高度,问这群牛中任意一头牛所能看到的其他牛的数量的总和。
思路
练手题,寻找往右数第一个比当前元素大的元素的位置,跑一遍单调递增栈即可。
代码实现

#include <cstdio>
#include <stack>
using namespace std;
const int maxn=8e4+10;
int n,ans=0,h[maxn];
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&h[i]);
	h[++n]=1e9+10;
	stack<int> s;
	int i=1;
	s.push(i++);
	while(!s.empty()&&i<=n){
		while(!s.empty()&&h[s.top()]<=h[i]){
			ans+=i-s.top()-1;
			s.pop();
		}
		s.push(i++);
	}	
	printf("%d",ans);
	return 0;
}

POJ2796 Feel Good
题目链接

题意
给定一个数组序列,要求从中找出一段区间,使得该区间的最小值 × 该区间的所有元素之和的值最大。
思路
假设我们定义这个值为 k k k ,然后遍历序列中的每一个数,以这个数为中心向左右两边扩展直至遇到比这个数更小的数为止,这样我们就可以得到以这个数为最小值的区间,然后计算该区间的 k k k,取所有区间中的最大值即为我们想要的结果,维护一个单调递增栈,在每次元素出栈的时候计算 k k k 值,当然,为了简化计算,我们可以先求出数组的前缀和,具体看代码。
代码实现

#include <cstdio>
#include <stack>
using namespace std;
typedef long long ll;
const int maxn=1e5+10;
ll l,r,n,ans,a[maxn],sum[maxn];
stack<int> s;

int main()
{
	scanf("%lld",&n);
	sum[0]=0;
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		sum[i]=sum[i-1]+a[i];
	} 
	a[++n]=0;sum[n]=sum[n-1];
	ans=0;l=r=1;
	int i=1;
	s.push(i++);
	while(!s.empty()&&i<=n){
		while(!s.empty()&&a[s.top()]>a[i]){
			int cur=s.top();
			s.pop();
			ll tmp=s.empty()?(ll)a[cur]*sum[i-1]:(ll)a[cur]*(sum[i-1]-sum[s.top()]);
			if(tmp>ans){
				ans=tmp;
				r=i-1,l=s.empty()?1:s.top()+1;
			}
		}
		s.push(i++);
	}
	printf("%lld\n%lld %lld",ans,l,r);
	return 0;
}

LeetCode 42 接雨水
题目链接

题意
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
在这里插入图片描述
思路
这道题的关键是理解积水量的计算方法,对于每一个柱子,其形成的积水量等于往右第一个比它高的元素和往左第一个比它高的元素的较小者减去自身高度后再乘以偏移量,理解了计算方法那么这道题目仅需维护一个单调递增栈即可解决,所有元素依次入栈,在每次出栈时计算积水量加和即可。

代码实现

class Solution {
public:
    int trap(vector<int>& height) {
        int n=height.size();
        int st[n+10],top=0,ans=0;
        st[0]=0;
        for(int i=0;i<n;i++){
            while(top>0&&height[st[top]]<height[i]){
                int nowh=height[st[top]];
                top--;
                if(top==0)
                    break;
                int maxh=min(height[i],height[st[top]]);
                ans+=(i-st[top]-1)*(maxh-nowh);
            }
            st[++top]=i;
        }
        return ans;
    }
};

POJ2559 Largest Rectangle in a Histogram
题目链接

题意
给定一个直方图,求直方图中的最大矩形的面积。例如,下面这个图片中直方图的高度从左到右分别是2, 1, 4, 5, 1, 3, 3, 他们的宽都是1,其中最大的矩形是阴影部分。
在这里插入图片描述
思路
对于每一个矩形高度,其可组成的最大矩形左右端点的范围即为以该矩形的高度为中心,向两边扩展直至遇到第一个高度小于它的矩形为止所组成的范围,暴力枚举的话时间复杂度为 O ( n 2 ) O(n^2) O(n2)肯定会超时,故采用单调栈来进行优化,具体做法是维护一个单调递减栈,经两次单调栈处理即可得到以某一高度为中心的左右端点,然后计算以每一个高度为中心的矩形的最大面积,取最大值即可,经优化后时间复杂度降为 O ( n ) O(n) O(n)

代码实现

#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
typedef long long ll;
const int maxn=100010;

int n;
int heights[maxn],st[maxn];

ll StackSolve()
{
	int top=0;
	int left[n+10],right[n+10];
	for(int i=0;i<n;i++){
		while(top>0&&heights[st[top]]>=heights[i])
			--top;
		left[i]=top>0?(st[top]+1):0;
		st[++top]=i;
	}
	top=0;
	for(int i=n-1;i>=0;i--){
		while(top>0&&heights[st[top]]>=heights[i])
			--top;			
		right[i]=top>0?(st[top]):n;
		st[++top]=i;
	}
	ll ans=0;
	for(int i=0;i<n;i++){
		ans=max(ans,(ll)(right[i]-left[i])*heights[i]);
	}
	return ans;
}

int main()
{
	while(~scanf("%d",&n)){
		if(n==0)
			break;
		for(int i=0;i<n;i++){
			scanf("%d",&heights[i]);
		}
		ll ans=StackSolve();
		printf("%lld\n",ans);		
	}
	return 0;
}

POJ2082 Terrible Sets
题目链接

题意
这道题的题意感觉特别绕,总结一下就是上一个题目的扩展,只不过是矩形的宽度变了。
思路
同上
代码实现

#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;

typedef long long ll;
const int maxn=50010;

int n,st[maxn],heights[maxn],width[maxn],sumw[maxn];

ll StackSolve()
{
	int top=0;
	int left[n+10],right[n+10];
	for(int i=0;i<n;i++){
		while(top>0&&heights[st[top]]>=heights[i]){
			--top;
		}	
		left[i]=top==0?0:(st[top]+1);
		st[++top]=i;
	}
	top=0;
	for(int i=n-1;i>=0;i--){
		while(top>0&&heights[st[top]]>=heights[i]){
			--top;			
		}
		right[i]=top==0?n:st[top];
		st[++top]=i;
	}
	ll ans=0;
	for(int i=0;i<n;i++){
		ans=max(ans,(ll)(sumw[right[i]]-sumw[left[i]])*heights[i]);
	}
	return ans;
}

int main()
{
	while(~scanf("%d",&n)){
		if(n==-1)
			break;
		for(int i=0;i<n;i++){
			scanf("%d %d",&width[i],&heights[i]);
			sumw[i+1]=sumw[i]+width[i];
		}
		ll ans=StackSolve();
		printf("%lld\n",ans);
	}
	return 0;
}


POJ3494 Largest Submatrix of All 1’s
题目链接

题意
给定一个 m × n m×n m×n 的0-1矩阵,求元素最多的全1子矩阵。
思路
最大直方图问题的小扩展,将矩阵按行截断即可转换为求最大直方图问题。
代码实现

#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
typedef long long ll;
int a,m,n,heights[2010],st[2010];

ll StackSolve()
{
	int nn=n;
	heights[nn++]=0;
    ll ans=0;
    int top=0;
    for(int i=0;i<nn;i++){
        while(top>0&&heights[st[top]]>heights[i]){
            ll cur=st[top];
            top--;
            if(top==0){
            	ll area=i*heights[cur];
            	ans=max(ans,area);
			}         	
            else{
            	ll area=(i-st[top]-1)*heights[cur];
            	ans=max(ans,area);
			}
        }
        st[++top]=i;
    }
    return ans;
}

int main()
{
	while(~scanf("%d %d",&m,&n)){
		ll ans=0;
		for(int i=0;i<m;i++){
			for(int j=0;j<n;j++){
				scanf("%d",&a);
				heights[j]=a?heights[j]+1:0;
			}
			ans=max(ans,StackSolve());
		}
		printf("%lld\n",ans);
	}
	return 0;
}

HDU5033 Building
题目链接

题意
N N N个摩天大楼依次排开,给出每个大楼的坐标与高度,要求求解给定坐标(假设高度为0)处所能看到天空(即不能被大楼遮挡)的最大角度。
思路
思想是栈中保留有用状态,去除冗杂状态,维护一个单调栈,将输入信息依次入栈:
(1)当高度大于等于栈顶高度时,弹栈
(2)当当前元素入栈后,形成的形状为凹形,弹栈
(3)否则,入栈
这道题需要自己画图,找出相邻两点的斜率之间的关系以及单调栈需要维护的形状(点之间连线所构成的图形形状)
代码实现

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
#define pi acos(-1.0)
#define eps 1e-8
typedef long long ll;
const int maxn=100010;
int t,n,q;

struct node{
	int idx;
	double x,h;
	node(){}
	node(int ii,double xx,double hh):idx(ii),x(xx),h(hh){}
	friend bool operator <(const node &n1,const node &n2){
		return n1.x<n2.x; 
	}
};

node a[2*maxn],st[2*maxn];
double ans[maxn],left[maxn],right[maxn];

double Slope(node &a,node &b){
	return fabs((a.h-b.h)/(a.x-b.x));
}

double Angle(double k){
	return atan(1/k)*180/pi;
}

bool Judge(node &a,node &b,node &c){
	return Slope(a,b)>Slope(b,c);
}

void StackSolve(){
	int top=0;
	for(int i=0;i<n+q;i++){
		if(a[i].h){
			while(top>0&&st[top].h<=a[i].h)	--top;
			while(top>=2&&Judge(st[top-1],st[top],a[i]))	--top;
			st[++top]=a[i];
		}
		else{
			while(top>=2&&Judge(st[top-1],st[top],a[i]))	--top;
			ans[a[i].idx]+=Angle(Slope(st[top],a[i]));
		}
	}
	top=0;
	for(int i=n+q-1;i>=0;i--){
		if(a[i].idx==-1){
			while(top>0&&st[top].h<=a[i].h)	--top;
			while(top>=2&&Judge(st[top-1],st[top],a[i]))	--top;
			st[++top]=a[i];
		}
		else{
			while(top>=2&&Judge(st[top-1],st[top],a[i]))	--top;
			ans[a[i].idx]+=Angle(Slope(st[top],a[i]));
		}		
	}
}

int main()
{
	scanf("%d",&t);
	for(int cnt=1;cnt<=t;cnt++){
		scanf("%d",&n);
		for(int i=0;i<n;i++){
			scanf("%lf %lf",&a[i].x,&a[i].h);
			a[i].idx=-1;
		}
		scanf("%d",&q);
		for(int i=0;i<q;i++){
			scanf("%lf",&a[i+n].x);
			a[i+n].h=0;a[i+n].idx=i;
		}
		sort(a,a+n+q);
		memset(ans,0,sizeof(ans));
		StackSolve();
		printf("Case #%d:\n",cnt);
		for(int i=0;i<q;i++){
            printf("%.10f\n",ans[i]);
        }
	}
	return 0;
}

本文中部分题目的选取与思路参考了以下blog:
https://blog.csdn.net/xtulollipop/article/details/52558686
https://www.hankcs.com/

  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值