算法—单调栈

1.单调栈简介

单调栈不是一种新的数据结构,它在结构上仍然是一个普通栈,只是在使用方法上有所区别。在栈的「先进后出」规则基础上,要求「从 栈底 到 栈顶 的元素是单调递增(或者单调递减)」。其中满足从栈底到栈顶的元素是单调递增的栈,叫做「单调递增栈」。满足从栈底到栈顶的元素是单调递减的栈,叫做「单调递减栈」。

单调栈的核心思想是:

及时去掉无用数据,保证栈中数据有序

2.单调栈的应用

1.寻找当前元素左侧,离它最近,并且比它大的元素在哪

从左往右遍历元素,构造⼀个单调递减的栈。插⼊当前位置的元素的时:

如果栈为空,则左侧不存在⽐当前元素⼤的元素;

如果栈⾮空,插⼊当前位置元素时的栈顶元素就是所找的元素。

因为我们要找的是最终结果的位置。因此,栈⾥⾯存的是每个元素的下标
以数组a[]为例
a[] = {1,4,10,6,3,3,15,21,8}

栈顶元素小于等于待插入元素 st.top() <= a[i] 

操作:删除栈顶元素

栈顶元素一定不是当前元素要找的数

(因为要找比自己大的元素,栈顶元素小于当前元素肯定不可以了)

栈顶元素必定不是后面元素要找的数

(栈顶元素都小于待插入元素了,从左往右遍历,就算后面元素要找比自己大的元素,离自己最近的也不会是栈顶元素,待插入元素比栈顶元素更近)

如果栈顶元素大于待插入元素 st.top() > a[i] 

操作:更新结果

栈顶元素就是i位置要找的数

把当前元素加入栈中

模版

#include <iostream>
#include <stack>
using namespace std;
const int N = 3e6 + 10;
int a[N], n;
int ret[N];

void test()
{
	stack<int> st; //维护一个单调递减的栈,栈里存的是元素的下标
	for (int i = 1;i <= n;i++)
	{
		while(st.size() && a[st.top()] <= a[i]) 
			st.pop();

		if (st.size()) 
			ret[i] = st.top();

		st.push(i);
	}
}
int main()
{
	cin >> n;
	for (int i = 1;i <= n;i++) cin >> a[i];
	test();
	for (int i = 1;i <= n;i++) cout << ret[i]<<" ";
	return 0;
}

2.寻找当前元素左侧,离它最近,并且比它小的元素在哪

从左往右遍历元素,构造⼀个单调递增的栈。插⼊当前位置的元素的时:
如果栈为空,则左侧不存在⽐当前元素⼩的元素;
如果栈⾮空,插⼊当前位置元素时的栈顶元素就是所找的元素。

同理:

栈顶元素大于等于待插入元素 st.top() >= a[i] 

操作:删除栈顶元素

栈顶元素一定不是当前元素要找的数

(因为要找比自己小的元素,栈顶元素大于等于当前元素肯定不可以了)

栈顶元素必定不是后面元素要找的数

(栈顶元素都大于待插入元素了,从左往右遍历,就算后面元素要找比自己小的元素,离自己最近的也不会是栈顶元素,待插入元素比栈顶元素更近)

如果栈顶元素大于待插入元素 st.top() < a[i] 

操作:更新结果

栈顶元素就是i位置要找的数

把当前元素加入栈中

模版

#include <iostream>
#include <stack>
using namespace std;
const int N = 3e6 + 10;
int a[N], n;
int ret[N];

void test()
{
	stack<int> st; //维护一个单调递减的栈,栈里存的是元素的下标
	for (int i = 1;i <= n;i++)
	{
		while(st.size() && a[st.top()] >= a[i]) 
			st.pop();

		if (st.size()) 
			ret[i] = st.top();

		st.push(i);
	}
}
int main()
{
	cin >> n;
	for (int i = 1;i <= n;i++) cin >> a[i];
	test();
	for (int i = 1;i <= n;i++) cout << ret[i]<<" ";
	return 0;
}

3.寻找当前元素右侧,离它最近,并且⽐它⼤的元素在哪 

与应用一类似,仅需改变遍历顺序,从右往左遍历,创建单调递减的栈

模版

#include <iostream>
#include <stack>
using namespace std;
const int N = 3e6 + 10;
int a[N], n;
int ret[N];

void test()
{
	stack<int> st; //维护一个单调递减的栈,栈里存的是元素的下标
	for (int i = n;i >= 1;i--)
	{
		while(st.size() && a[st.top()] <= a[i]) 
			st.pop();

		if (st.size()) 
			ret[i] = st.top();

		st.push(i);
	}
}
int main()
{
	cin >> n;
	for (int i = 1;i <= n;i++) cin >> a[i];
	test();
	for (int i = 1;i <= n;i++) cout << ret[i]<<" ";
	return 0;
}

4.寻找当前元素右侧,离它最近,并且比它小的元素在哪 

与应用2类似,仅需改变遍历顺序,从右往左遍历,创建单调递增的栈

#include <iostream>
#include <stack>
using namespace std;
const int N = 3e6 + 10;
int a[N], n;
int ret[N];

void test()
{
	stack<int> st; //维护一个单调递减的栈,栈里存的是元素的下标
	for (int i = n;i >= 1;i--)
	{
		while(st.size() && a[st.top()] >= a[i]) 
			st.pop();

		if (st.size()) 
			ret[i] = st.top();

		st.push(i);
	}
}
int main()
{
	cin >> n;
	for (int i = 1;i <= n;i++) cin >> a[i];
	test();
	for (int i = 1;i <= n;i++) cout << ret[i]<<" ";
	return 0;
}

 总结:

找左侧,正遍历;找右侧,逆遍历;

比它大,单调减;比它小,单调增;

3.经典例题

https://www.luogu.com.cn/problem/SP1805

思路:

最后大矩形的上边界一定是某个小矩形的顶

那么我们就枚举每一个小矩形,将其顶作为限制条件尽可能地向两边扩展

我们枚举每一个小矩形i,如上图,枚举到 i 位置时,向左找离它最近的且比它小的值, 记录所在位置x,再向右找离它最近且最小的值,记录所在位置y 。大矩形的面积就是图示红色的部分。

面积为 i 位置小矩形的高度 乘以 宽度(y-x+1)。

所以用单调栈就可以解决,代码如下

#include<iostream>
#include<stack>
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
LL n;
LL h[N],x[N],y[N];

int main()
{
	while (cin >> n && n)
	{
		for (int i = 1;i <= n;i++)
		{
			cin >> h[i];
		}
		// 找左边第一个比自己小
		stack<LL> st;
		for (int i = 1;i <= n;i++)
		{
			while (st.size() && h[st.top()] >= h[i])
				st.pop();

			if (st.size())
				x[i] = st.top();
			else
				x[i] = 0;
			st.push(i);
		}
		
		while (st.size()) st.pop(); //清空栈内数据

        // 找右边第一个比自己小
		for (int i = n;i >= 1;i--)
		{
			while (st.size() && h[st.top()] >= h[i])
				st.pop();

			if (st.size())
				y[i] = st.top();
			else
				y[i] = n+1;  
			st.push(i);
		}
		LL ret = -1;
		for (int i = 1;i <= n;i++)
		{
			ret = max(ret, h[i] * (y[i] - x[i] - 1));
		}
		cout << ret << endl; 
	}

	return 0;
}

<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/) 通过练习上述题目,可以深入掌握单调栈的变种应用和边界处理技巧。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值