【算法】单调栈

本文详细介绍了单调栈的概念、伪代码以及在不同问题中的应用,包括模板题、视野总和问题、柱状图中的最大矩形和最大区间问题的解题思路和代码实现。通过实例展示了单调栈如何帮助解决复杂问题,并提供了相应的算法优化技巧。

单调栈的定义:

从名字上就能猜出来,这种数据结构在栈的基础上,栈内的元素是单调有序的,所以单调栈分为单调递增栈和单调递减栈(增减性的划分是根据栈顶到栈底的元素变化规律(搞不懂为什么定义要从栈顶开始看,栈是从栈底开始加入元素的呀,好像有一点反思维)

  • 单调递增栈: 从栈顶往栈底看,是单调递增的关系(含相等)
  • 单调递减栈: 从栈顶往栈底看,是单调递减的关系(含相等)

例子:现在有一组数:3,4,2,6,4,5,2,3,如果从左往右依次进单调递增栈,那么是这样操作的:在这里插入图片描述

伪代码:

以单调递增栈为例:

stack<int> st;
//此处一般需要给数组最后添加结束标志符
for (遍历这个数组)
{
	// 如果栈不为空且栈顶元素比压栈元素小,则栈顶元素出栈并作计算与更新结果,然后继续下一轮比较,直至该元素可以入栈;
    // 否则,(即栈为空,或栈顶元素大于等于压栈元素),直接入栈即可
    while ( !st.empty() && vec[st.top()] < vec[i] ) {
        st.pop();
        // Do something
    }
    st.push(i);  // 数组内的元素都要入栈,但入栈的是其下标
}

注意事项:

  • 为了保证栈内元素最后都能出栈,需要在数组结尾添加一个特殊的数字:
    对单调递增栈,在数组末尾添加一个最大数,如 INT_MAX; 对单调递减栈,在数组末尾添加一个最小数,如 INT_MIN.
  • 数组中的所有元素都要入栈一次和出栈一次,除了最后一个特殊元素;
  • 当一个元素出栈的时候,做计算,更新答案;
  • 当一个元素即将出栈时,记住整个栈内元素具有单调性,很多时候需要利用这个特性去做计算;
  • 一个元素可能会令栈中若干个元素出栈,但也可能是直接入栈

应用

1.模板题

洛谷P5788 【模板】单调栈

题目:
在这里插入图片描述
解题思路:
求每个数后第一个大于他的数。
直接看数字不太容易想,那么我们把题目转化一下:有 n n n 个人,每个人向右上方看,求她看到的第一个人。
在这里插入图片描述
比如第2个人,他的高度是4,那么他往右上方看,肯定看不到第3,第4个人,因为他们比他矮,被遮住了,那留着也没用,直接舍弃就好(我没有歧视矮,我也想长高QwQ)。那么大家是否看到了单调性?比他矮的人全部舍弃,留下的都是比他高的。所以我们可以用单调栈实现。由于是往右看,所以需要从后往前遍历数组。

代码:

#include <bits/stdc++.h>
#include <iostream>
#include <string.h>
#include <algorithm>
#define ll long long
#define qc ios::sync_with_stdio(false); cin.tie(0);cout.tie(0)
using namespace std;
const int MAXN = 3e6 + 7;
const int inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f;
int a[MAXN],ans[MAXN];
stack<int> s;//单调递增栈 

int main()
{
	qc;
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i]; 
	for(int i=n;i>0;i--)
	{
		while(!s.empty() && a[s.top()]<=a[i])
			s.pop();
		ans[i]=s.empty()? 0:s.top();
		s.push(i);//压入下标
	}
	for(int i=1;i<n;i++)
		cout<<ans[i]<<" ";
	cout<<ans[n]<<endl;
	return 0;
}

2.视野总和问题

POJ-3250 Bad Hair Day

题目:
在这里插入图片描述
在这里插入图片描述
题目大意:
一群高度不完全相同的牛从左到右站成一排,每头牛只能看见它右边的比它矮的牛的发型,若遇到一头高度大于或等于它的牛,则无法继续看到这头牛和后面的其他牛的发型。给出这些牛的高度,要求每头牛可以看到的牛的数量的和。

解题思路1:
每个元素只会出栈一次,所以当他出栈的时候就可以计算出他可以看到的牛的个数了,即当前所在下标与自己下标的差值。

代码1:

#include <iostream>
#include <string.h>
#include <algorithm>
#include <stack>
#define ll long long
#define qc ios::sync_with_stdio(false); cin.tie(0);cout.tie(0)
using namespace std;
const int MAXN = 3e6 + 7;
const int inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f;
int a[MAXN];
stack<int> s;//单调栈 

int main()
{
//	freopen("in.txt", "r", stdin);
	qc;
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i]; 
	n++;
	a[n]=inf;
	ll ans=0;//千万记得开ll,不然就像我一样,debug一个小时,QwQ
	for(int i=1;i<=n;i++)
	{
		while(!s.empty() && a[s.top()]<=a[i])
		{
			ans+=(i-s.top()-1);
			s.pop();
		}	
		s.push(i);
	}
	cout<<ans<<endl;
	return 0;
}

解题思路2:(一开始找不到第一个思路的bug时,看其他博客的思路找到的)
可以把问题等效成每头牛可以被看到的次数之和。
构建一个单调递增栈,栈底元素最大,从左到右依次读取牛的高度,当待入栈元素小于栈顶元素时,代表目前栈里的牛都可以看到这头牛,答案加上目前栈里的元素的个数,然后把该元素入栈,继续下一个比较;如果待入栈元素大于等于栈顶元素时,就要从栈中弹出比它小的,即删除不能看到这头牛的元素,答案加上目前栈里所剩元素的个数,然后把该元素入栈,继续下一个比较。注意!当你计算栈中元素个数时,要记得减去特殊元素哦!!!

代码2:

#include <iostream>
#include <string.h>
#include <algorithm>
#include <stack>
#define ll long long
#define qc ios::sync_with_stdio(false); cin.tie(0);cout.tie(0)
using namespace std;
const int MAXN = 80000 + 7;
const int inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f;
int a[MAXN];
stack<int> s;//单调栈 

int main()
{
//	freopen("in.txt", "r", stdin);
	qc;
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i]; 
	s.push(inf);//压入特殊元素 
	ll ans=0;
	for(int i=1;i<=n;i++)
	{
		while(!s.empty() && s.top()<=a[i])
		{
			s.pop();
		}
		ans+=(s.size()-1);//有一个特殊元素要减去 
		s.push(a[i]);
	}
	cout<<ans<<endl;
	return 0;
}

3.柱状图中的最大矩形

Acwing 131. 直方图中最大的矩形

题目:
在这里插入图片描述
在这里插入图片描述
解题思路:
维护一个单调递减栈,即从栈底到栈顶依次增大,这样栈中的元素往右扩展,一定可以成为更大的矩形。如题目中的图所示,当该元素要被弹出栈的时候,那么就可以计算以该元素为高往右最大扩展的矩形的面积了。计算出面积后,一直更新最大值,答案就出来了。

代码:

#include <bits/stdc++.h>
#include <iostream>
#include <string.h>
#include <algorithm>
#define ll long long
#define qc ios::sync_with_stdio(false); cin.tie(0);cout.tie(0)
using namespace std;
const int MAXN = 2e5 + 7;
const int inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f;
vector<int> heights;

ll largestRectangleArea() 
{
    heights.push_back(INT_MIN);//特殊元素 
    ll result = 0;
    stack<int> st;
    
    ll h=0, w=0;
    int top_id = 0;

    for (int i=0; i<heights.size(); ++i)
    {
        // 单调栈内元素是递增的 
        while ( !st.empty() && heights[st.top()] > heights[i] ) {
            top_id = st.top();
            st.pop();
            h = heights[top_id];
            if (st.empty()) //栈为空,代表是目前最小的,所以0~i-1所有都可以满足 
                w = i;
            else 
                w = i - st.top() - 1;//i:将要入栈的下标,st.top():下一个可能出栈的元素小标 
      
            result = max(result, w*h);
        }
        st.push(i);
    }
    return result;
}

int main()
{
//	freopen("in.txt", "r", stdin);
	qc;
	int n,tmp;
	while(cin>>n)
	{
		if(n==0)
			break;
		while(!heights.empty())//清空vector 
			heights.pop_back();
		for(int i=0;i<n;i++)
		{
			cin>>tmp;
			heights.push_back(tmp);
		}
		ll ans=largestRectangleArea();
		cout<<ans<<endl;
	} 
	return 0;
}

题目变形:

洛谷 P4147 玉蟾宫

解题思路:

  • 按行去划分,O(n)枚举每一行,对该行即以上的部分做最大子矩阵处理;
  • 用pos数组记录每行向上可延伸的最大距离,预处理的方式为:
    (1)读到一个‘F’,该处pos=上一行该列pos的值+1;
    (2)读到一个‘R’,该处pos=0(因为该处不可向上伸展);

代码:

#include <bits/stdc++.h>
#include <iostream>
#include <string.h>
#include <algorithm>
#define ll long long
#define qc ios::sync_with_stdio(false); cin.tie(0);cout.tie(0)
using namespace std;
const int MAXN = 2e5 + 7;
const int inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f;
int n,m,pos[1010][1010],maxs=0;
ll ans;
vector<int> heights;

ll largestRectangleArea(int k) 
{
	while(!heights.empty())
		heights.pop_back();
	for(int i=1;i<=m;i++)
		heights.push_back(pos[k][i]);
    heights.push_back(INT_MIN);//特殊元素 
    ll result = 0;
    stack<int> st;
    ll h=0, w=0;
    int top_id = 0;

    for (int i=0; i<heights.size(); ++i)
    {
        // 单调栈内元素是递增的 
        while ( !st.empty() && heights[st.top()] > heights[i] ) {
            top_id = st.top();
            st.pop();
            h = heights[top_id];
            if (st.empty()) //栈为空,代表是目前最小的,所以0~i-1所有都可以满足 
                w = i;
            else 
                w = i - st.top() - 1;//i:将要入栈的下标,st.top():下一个可能出栈的元素小标 
      
            result = max(result, w*h);
        }
        st.push(i);
    }
    return result;
}

int main()
{
//	freopen("in.txt", "r", stdin);
	qc;
	char c;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			cin>>c;
			if(c=='F')
				pos[i][j]=pos[i-1][j]+1;
		}
	}
	ll tmp=0;
	for(int i=1;i<=n;i++)
	{
		tmp=largestRectangleArea(i);
		ans=max(tmp,ans);
	}
	cout<<3*ans<<endl;
	return 0;
}

4.最大区间

POJ 2796 Feel Good

题目:
在这里插入图片描述
在这里插入图片描述

题目大意:

给你一组数字,求一区间,使得区间元素和乘以区间最小值最大,结果要求输出这个最大值和区间的左右端点

解题思路:
首先应该能想到暴力的做法。想要求出答案,那么对于每一个以 a i a_i ai 为最小的元素的区间,它的左右端点都是比它小的数(为了方便,用开区间来描述)。所以只要以 a i a_i ai 的起点向左右扩展就行了,这样的复杂度是 O ( n 2 ) O(n^2) O(n2)
那么如何来优化呢?计算区间的和用前缀和就行了,那么区间的端点呢?可以用单调栈来解决。我们用单调栈来维护区间最小值,那么对当前元素来说,在它之前的一个最小值和在它之后的一个最小值就是区间的两个端点。所以我们用一个单调递增栈从前到后面扫一遍,就求出 a i a_i ai 左边比 a i a_i ai 小的一个元素,同样倒着扫一遍就能求出另一边的元素。

代码:

//#include <bits/stdc++.h>
#include <iostream>
#include <string.h>
#include <algorithm>
#include <stack>
#define ll long long
#define qc ios::sync_with_stdio(false); cin.tie(0);cout.tie(0)
using namespace std;
const int MAXN = 1e5 + 7;
const int inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f;
const int N=1e5+100;
const int maxn = 1e5+10;
ll arr[MAXN], pre[MAXN];
int n, l[MAXN], r[MAXN];
stack<int> st;
int main() {
    scanf("%d", &n);
    for (int i = 1; i<=n; ++i) {
        scanf("%lld", &arr[i]);
        pre[i] += pre[i-1]+arr[i];
    }
    for (int i = 1; i<=n; ++i) {
        while(!st.empty() && arr[st.top()]>=arr[i]) st.pop();
        l[i] = st.empty() ? 1 : st.top()+1;
        st.push(i);
    }
    while(!st.empty()) st.pop();
    for (int i = n; i>=1; --i) {
        while(!st.empty() && arr[st.top()]>=arr[i]) st.pop();
        r[i] = st.empty() ? n : st.top()-1;
        st.push(i);
    }
    ll maxx = -1; 
	int pl,pr;
    for (int i = 1; i<=n; ++i)
        if (maxx < (ll)arr[i]*(pre[r[i]]-pre[l[i]-1])) {
            maxx = (ll)arr[i]*(pre[r[i]]-pre[l[i]-1]);
            pl=l[i];
			pr=r[i];
        }
    cout << maxx << "\n" << pl << " " << pr << endl; 
    return 0;
}

碎碎念:

这篇算法总结花了我好大的心血啊…一道一道到原题,还有因为没开long long找了1小时的bug…终于完工了!睡觉睡觉,大家晚安呐!

<think>嗯,用户让我介绍一下单调栈算法题,我需要先理解他们可能的需求。可能他们是在准备面试,或者在学习数据结构和算法时遇到了困难。首先,我得明确单调栈的定义和基本概念,然后解释它的应用场景,再给出一些经典例题,这样用户能更好地理解。 首先,单调栈是什么?它是一种特殊的栈结构,里面的元素按照某种顺序排列,比如递增或递减。这种结构通常用来解决需要寻找下一个更大或更小元素的问题。比如,LeetCode上的“每日温度”或者“柱状图中最大的矩形”这类题目。 接下来,我需要讲清楚单调栈的工作原理。比如,当遍历数组时,如何维护栈内元素的单调性。比如,当寻找下一个更大元素时,栈应该是单调递减的。遇到比栈顶大的元素时,就弹出栈顶,并记录结果。这个过程需要详细说明,可能还要举个具体的例子,比如用数组[73,74,75,71,69,72,76,73]来说明每日温度的问题。 然后,应用场景部分需要列举几个常见的问题类型,比如找下一个更大元素、上一个更小元素、或者处理矩形面积等。每个场景最好对应一个例题,并简要说明解决思路。例如,柱状图中最大的矩形,这题需要用单调栈来找到每个柱子左右两边第一个比它矮的柱子,从而计算面积。 可能用户还会想知道实现时的注意事项,比如栈中存储的是元素的值还是索引?通常是索引,因为需要计算位置差或者处理元素的位置关系。另外,处理循环数组的情况,可能需要将数组长度翻倍,或者使用取模的方法来模拟循环。 还有时间复杂度的分析,单调栈通常是O(n),因为每个元素最多入栈和出栈一次。这点需要强调,因为它比暴力解法高效很多。 在编写过程中,要确保例子清晰,步骤详细。比如,用具体的数字一步步推演栈的变化和结果的计算过程。这样用户能够直观地看到每一步发生了什么,理解为什么单调栈有效。 此外,可能需要提醒用户常见的错误,比如在比较元素大小时忘记处理栈为空的情况,或者在处理边界条件时出错。例如,当所有元素都没有下一个更大元素时,应该如何处理返回值。 最后,总结一下单调栈的核心思想,以及解决问题的步骤:确定单调方向、遍历元素、维护栈结构、计算结果。并鼓励用户通过练习题目来巩固理解,推荐一些经典的题目供他们练习。 现在,我需要把这些思考组织成一个结构清晰的回答,分点说明,使用例子来辅助解释,确保用户能够逐步理解单调栈的原理和应用。</think>### 单调栈算法详解 #### 一、基本概念 **单调栈**是一种特殊的栈结构,其内部元素始终保持单调递增或递减的顺序。常用于解决需要寻找元素**下一个更大(或更小)元素**的问题。 #### 二、核心原理 1. **单调性维护** - 递增栈:栈底到栈顶元素递增,用于寻找下一个更小元素。 - 递减栈:栈底到栈顶元素递减,用于寻找下一个更大元素。 2. **操作流程** 遍历数组时,比较当前元素与栈顶元素: - 若符合单调性,压入栈。 - 若破坏单调性,弹出栈顶并记录结果,直到满足单调性。 #### 三、典型应用场景 1. **下一个更大元素** - 例题:[LeetCode 739. 每日温度](https://leetcode.com/problems/daily-temperatures/) - 示例:数组 `[73,74,75,71,69,72,76,73]`,求每个元素下一次更高温度的天数差。 - 过程: ```text 索引 元素 栈操作 结果 0 73 入栈 [ ] 1 74 弹出73,入74 res[0]=1-0=1 2 75 弹出74,入75 res[1]=2-1=1 3 71 入栈 [75,71] 4 69 入栈 [75,71,69] 5 72 弹出69、71,入72 res[4]=5-4=1, res[3]=5-3=2 6 76 弹出72、75,入76 res[5]=6-5=1, res[2]=6-2=4 7 73 入栈 [76,73] ``` - 结果:`[1,1,4,2,1,1,0,0]` 2. **最大矩形面积** - 例题:[LeetCode 84. 柱状图中最大的矩形](https://leetcode.com/problems/largest-rectangle-in-histogram/) - 核心思路:对每个柱子,找到左右第一个比它矮的柱子,确定宽度边界。 3. **循环数组问题** - 处理方式:将数组长度翻倍遍历,或索引取模模拟循环。 #### 四、代码模板(以递减栈为例) ```python def monotonic_stack(nums): stack = [] res = [0] * len(nums) for i in range(len(nums)): while stack and nums[i] > nums[stack[-1]]: top = stack.pop() res[top] = i - top stack.append(i) return res ``` #### 五、复杂度分析 - **时间复杂度**:$O(n)$,每个元素最多入栈和出栈一次。 - **空间复杂度**:$O(n)$,栈空间最坏情况存储全部元素。 #### 六、注意事项 1. 栈中通常存储**索引**而非值,便于计算位置差。 2. 遍历结束后,栈中剩余元素需单独处理(如无后续更大元素时设为0)。 #### 七、经典习题推荐 1. [下一个更大元素 I](https://leetcode.com/problems/next-greater-element-i/) 2. [接雨水](https://leetcode.com/problems/trapping-rain-water/) 3. [子数组的最小值之和](https://leetcode.com/problems/sum-of-subarray-minimums/) 通过练习上述题目,可以深入掌握单调栈的变种应用和边界处理技巧。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值