目录
[1. 单调栈是什么?](#1. 单调栈是什么?)
[2. 单调栈解决的问题](#2. 单调栈解决的问题)
[1. 寻找当前元素左侧,离它最近,并且比它大的元素在哪](#1. 寻找当前元素左侧,离它最近,并且比它大的元素在哪)
[2. 寻找当前元素左侧,离它最近,并且比它小的元素在哪](#2. 寻找当前元素左侧,离它最近,并且比它小的元素在哪)
[3. 寻找当前元素右侧,离它最近,并且比它大的元素在哪](#3. 寻找当前元素右侧,离它最近,并且比它大的元素在哪)
[4. 寻找当前元素右侧,离它最近,并且比它小的元素在哪](#4. 寻找当前元素右侧,离它最近,并且比它小的元素在哪)
[1. P5788 【模板】单调栈 - 洛谷](#1. P5788 【模板】单调栈 - 洛谷)
[2. P1901 发射站 - 洛谷](#2. P1901 发射站 - 洛谷)
[3. SP1805 HISTOGRA - 直方图中的最大矩形](#3. SP1805 HISTOGRA - 直方图中的最大矩形)
一、认识单调栈
1. 单调栈是什么?
单调栈,顾名思义,就是一个具有单调性的栈。它是一种栈的数据结构,在这个栈存的元素,要么只能递增,要么只能递减。这种结果的实现很容易,最重要的就是理解维护这样一个单调栈的意义是什么?才能知道它该解决什么样的问题情况。
2. 单调栈解决的问题
通过单调栈,我们可以解决的问题比较局限,最基本的问题就是:"寻找最近更大/更小元素"的问题。而这类问题,我们可以分为4种情况:
- 寻找当前元素左侧,离它最近,并且比它大的元素在哪;
- 寻找当前元素左侧,离它最近,并且比它小的元素在哪;
- 寻找当前元素右侧,离它最近,并且比它大的元素在哪;
- 寻找当前元素右侧,离它最近,并且比它小的元素在哪。
虽然是四个问题,但是原理是⼀致的。因此,只要解决⼀个,举⼀反三就可以解决剩下的几个
二、解决问题的原理
前两个比较详细,后两个只需要修改一下遍历顺序即可。
1. 寻找当前元素左侧,离它最近,并且比它大的元素在哪
问题(一种问法而已)
给定一个数n表示数组的元素个数,一个大小为n整数数组 arr,其中 arr i 表示第 i + 1 个元素。对于每一个位置的元素,你需要找到它 左侧 离它最近、且大小 严格大于 它的数值的位置下标。 如果左侧没有比它更大的数,则返回 0。
示例:
cpp输⼊: 9 1 4 10 6 3 3 15 21 8 输出: 0 0 0 3 4 4 0 0 8
解决原理
对于寻找大于自己的元素位置,我们使用的是一个单调递减的栈,寻找左边的数,则我们需要从左向右遍历。通过维护栈顶元素是距离当前元素最近的值的一个栈来实现寻找。
我们假设 ret i 表示 arr i 的左边最近且更大的元素值,st表示一个栈。以数组为数据:1 4 10 6 3 3 15 21 8 为例。我们来模拟一遍:
- 当访问到第一个元素 1 的时候,此时栈为空,表示当前元素的左边没有严格比它大的元素,所以 ret1 = 0。然后会将1入栈。
- 当访问到第二个元素 4 的时候,此时栈的中存在一个元素1(栈顶),此时栈顶元素 st.top() < arr i ,也就是说在当前栈顶的元素就不是 arr i 的解,此时就可以将栈顶元素出栈,同时更新结果ret 2 = 0, 然后再将当前的元素 4 入栈。(此时将1出栈是因为如果在后续也一个元素是以1作为最解的话,设这个数是x(值可以为0),那么x 一定会小于刚刚入栈的4,此时4又是靠右更近,所以 4 应该才是x更近的解,因此这里的1就没有意义了,可以大胆将1出栈)。
- 当访问到第三个元素 10 的时候,此时栈中只有一个数 4 (栈顶),那么此时仍然是 st.top() < arr i ,同理,当前栈顶的元素就不是 arr i 的解,就将栈顶出栈,同时让 ret 0 = 0,再让 10 入栈。
- 当访问到第四个元素 6 时,此时栈中只有一个数 10 (栈顶),所以此时 st.top() > arr i ,满足条件,又因为我们之前将必定不是最终结果的元素都出栈了,所以此时arr i 的左边大于自己最近的值就是 10 ,所以就可以更新结果 ret i = 10,然后将 10 入栈(因为6可能会是后面某些元素的解)。
- 当访问到第五个元素 3 时,此时栈中有两个数 10 ,6 (6是栈顶),此时满足 st.top() > arr i ,所以当前 3 的最近更大的元素就是 6 ,所以更新结果 ret 5 = 6,最后需要将 3 入栈。
- 当访问到第六个元素 3 时,此时栈中有两个数 10 ,6 ,3(3是栈顶),此时st.top() == arr i 了,因此不满足大于的条件,所以需要继续向栈底去找,即先将3出栈,此时栈顶就是6,满足条件,然后就更新结果 ret 6 = 6,最后也需要将3入栈。
- 后续,同理...
所以,对于这道题,我们维护的是一个单调递减的栈,没当我们遍历到一个元素时,都可以根据这个栈中的数据进行确认是否有自己要找的数,维护思路为:
- 当我们遍历到一个元素时,如果栈为空,说明当前元素的左边没有满足大于自己的数,然后需要将自己入栈(因为自己可能是后面某个元素要找的数)。
- 当访问到当前元素大于或等于 栈顶元素时,说明此时当前栈顶元素不是我们要找的数,需要继续在栈中寻找,即将栈顶元素出栈(原因见上述模拟过程),直到出现满足大于当前值的数或者是栈为空,最后更新结果。
- 当访问到当前元素小于栈顶元素时,此时栈顶元素就是我们要找的数,我们直接更新结果就行,同时也要将该元素入栈(因为自己可能是后面某个元素要找的数)。
最后,注意:由于我们题目中我们需要找的是最近更大元素的位置(即下标),所以我们这里更新结果更新的应该是下标,所以我们栈中存的应该也是下标,那么我们每次取栈顶的时候,都是通过先访问栈顶中的下标,在通过下标来获取元素值的。
总结一下,这里的实验思路就是:
从左向右遍历数组。
- 如果栈为空,则表示左侧不存在比当前元素大的元素,只需要将当前元素下标入栈,再更新结果为0即可;
- 如果栈为非空,则需要判断,如果栈顶元素小于当前元素,则不断出栈,直到栈为空或满足条件,则更新结果(如果栈为空了,则更新为0;否则,则更新下标),最后也要将当前元素下标入栈。
所以上述我们实现的代码思路为:
cpp
#include <iostream>
#include <stack>
using namespace std;
const int N = 1e3 + 10;
int n;
int arr[N];
int ret[N]; // 存储第i个元素的解, 0表示无解
// 模板1:寻找当前元素左侧,离它最近,并且比它大的元素在哪
void test1()
{
stack<int> st; // 单调递减栈 - 存的下标
for (int i = 1; i <= n; i++)
{
while (st.size() && arr[st.top()] <= arr[i]) st.pop(); // 寻找解
if (st.size()) ret[i] = st.top(); // 如果栈空,则表示无解;栈不空,则更新结果
st.push(i); // 将下标入栈
}
// 验证一下结果
for (int i = 1; i <= n; i++) cout << ret[i] << ' ';
cout << endl;
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++) cin >> arr[i];
test1();
return 0;
}
2. 寻找当前元素左侧,离它最近,并且比它小的元素在哪
和上述不同,此时的寻找的是左边离他最近的比它小的数的位置。还是以上述问题为例(只是结果会变了),其示例如下所示:
cpp
输⼊:
9
1 4 10 6 3 3 15 21 8
输出:
0 1 2 2 1 1 6 7 6
解决思路:
首先注意,寻找小于自己的元素位置,我们使用的是一个单调递增的栈,寻找左边的数,则我们需要从左向右遍历。
假设 ret i 表示 arr i 的左边最近且更大的元素值的下标,st表示一个栈(存的是下标),对于这个示例中数据,从左向右遍历:
- 遇到 1 ,栈为空,没有满足小于它的数,更新ret 1 = 0(这里下标从1开始,这里使用0表示没有满足条件的数的位置),最后将 1 的下标 1 入栈。
- 遇到 4 ,栈的数据为 1 ,栈顶元素为 arr1 = 1 < 4 ,满足小于自己的条件,更新结果ret 2 = 1 (即 4 的左边最近,且小于它的数的下标是1),最后将 4 的下标 2 入栈。
- 遇到 10,栈的数据为 1,2 ,栈顶元素为 arr2 = 4 < 10 ,满足条件,更新结果 ret 3 = 2 (即 10 的左边最近,且小于它的数的下标是 2),最后将 10 的下标 3 入栈。
- 遇到 6,栈的数据为 1,2,3 ,栈顶元素为 arr3 = 10 > 6 ,不满足条件,则将10的下标3出栈;此时栈的数据为 1,2 ,栈顶元素为 arr2 = 4 < 6,满足条件,更新结果 ret 4 = 2 (即 6 的左边最近,且小于它的数的下标是 2),最后将 6 的下标 4 入栈。
- 遇到 3,栈的数据为 1,2,4 ,栈顶元素为 arr4 = 6 > 3 ,不满足条件,则将 6 的下标 4 出栈;此时栈的数据为 1,2 ,栈顶元素为 arr2 = 4 > 6,不满足条件,继续出栈;此时栈的数据为 1 ,栈顶元素为 arr1 = 1 < 3 ,满足条件,更新结果 ret 5 = 1 (即此时 3 的左边最近,且小于它的数的下标是 1),最后将 3 的下标 5 入栈。
- 遍历到第6个数,遇到 3,栈的数据为 1,5 ,栈顶元素为 arr3 = 3 等于 3 ,不满足条件,需要出栈;此时栈的数据为 1 ,栈顶元素为 arr1 = 1 < 3 ,满足条件,更新结果 ret 6 = 1 (即此时 3 的左边最近,且小于它的数的下标是 1),最后将 3 的下标 6 入栈。
- 后续,同理......
- 最后 ret i 中存的就是第i个数左边离他最近的比它小的数的位置。
注意:其中当当前元素的值 小于 栈顶元素 时,执行出栈操作的原因为:假设栈顶元素为 st.top() ,当前元素为arr i ,如果不出栈,也就意味着在arr i 的后面存在一个数 x 是大于此时的 st.top() 的,所以 x > st.top() > arr i ,但是 arr i 比 st.top() 更靠右,所以 x 找的数一定是 arr i ,不会是st.top(),所以此时st.top() 就没有意义了,就需要出栈。
通过这个过程,应该可以看出来,如果要找左边离他最近的比它小的数的位置,关键思路如下:
- 从左向右遍历
- 如果栈为空,则表示没有满足条件的数,将当前元素的下标入栈,再更新结果为0即可;
- 如果栈非空,则需要判断,如果栈顶元素(arr[st.top())大于当前元素的值,则不满足条件,进行出栈(直到栈为空或者满足条件),然后就更新结果(如果栈为空了,则更新为0;否则,则更新下标),最后将当前遍历的元素下标入栈。
实现代码为:
cpp
#include <iostream>
#include <stack>
using namespace std;
const int N = 1e3 + 10;
int n;
int arr[N];
int ret[N]; // 存储第i个元素的解, 0表示无解
// 模板2:寻找当前元素左侧,离它最近,并且⽐它⼩的元素在哪
void test2()
{
stack<int> st; // 单调递增栈 - 存的下标
for (int i = 1; i <= n; i++)
{
while (st.size() && arr[st.top()] >= arr[i]) st.pop(); // 寻找解
if (st.size()) ret[i] = st.top(); // 如果栈空,则表示无解;栈不空,则更新结果
st.push(i); // 将下标入栈
}
// 验证一下结果
for (int i = 1; i <= n; i++) cout << ret[i] << ' ';
cout << endl;
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++) cin >> arr[i];
test2();
return 0;
}
3. 寻找当前元素右侧,离它最近,并且比它大的元素在哪
寻找当前元素右侧,离它最近,并且比它大的元素在哪?对于这个问题,如果我们将数组逆序一下,则问题就可以转化为:寻找当前元素左侧,离它最近,并且比它大的元素在哪?所以这个问题和第一个问题的代码很像,只是这个问题需要从右向左遍历。
所以,实现代码即:
cpp
#include <iostream>
#include <stack>
using namespace std;
const int N = 1e3 + 10;
int n;
int arr[N];
int ret[N]; // 存储第i个元素的解, 0表示无解
// 模板3:寻找当前元素右侧,离它最近,并且⽐它⼤的元素在哪
void test3()
{
stack<int> st; // 单调递减栈 - 存的下标
for (int i = n; i >= 1; i--)
{
while (st.size() && arr[st.top()] <= arr[i]) st.pop(); // 寻找解
if (st.size()) ret[i] = st.top(); // 如果栈空,则表示无解;栈不空,则更新结果
st.push(i); // 将下标入栈
}
// 验证一下结果
for (int i = 1; i <= n; i++) cout << ret[i] << ' ';
cout << endl;
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++) cin >> arr[i];
test4();
return 0;
}
4. 寻找当前元素右侧,离它最近,并且比它小的元素在哪
这个问题就可以转化为:寻找当前元素左侧,离它最近,并且比它小的元素在哪?只需要将第2个问题的代码遍历顺序改为逆序即可,代码实现为:
cpp
#include <iostream>
#include <stack>
using namespace std;
const int N = 1e3 + 10;
int n;
int arr[N];
int ret[N]; // 存储第i个元素的解, 0表示无解
// 模板4:寻找当前元素右侧,离它最近,并且⽐它⼩的元素在哪
void test4()
{
stack<int> st; // 单调递减栈 - 存的下标
for (int i = n; i >= 1; i--)
{
while (st.size() && arr[st.top()] >= arr[i]) st.pop(); // 寻找解
if (st.size()) ret[i] = st.top(); // 如果栈空,则表示无解;栈不空,则更新结果
st.push(i); // 将下标入栈
}
// 验证一下结果
for (int i = 1; i <= n; i++) cout << ret[i] << ' ';
cout << endl;
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++) cin >> arr[i];
test4();
return 0;
}
三、单调栈的使用(简单记忆)★★★
上面我们的推到过程比较麻烦,这里总结了一下单调栈的简单记忆方法:
总结:
- 如果是找左侧,则正遍历;如果找右侧,则逆遍历。
- 如果是找比它大,则是单调递减栈;如果找比它小,则单调递增栈。
代码实现的简单记忆:
- 找左侧比它大:正遍历,找比它大就要把比它小或等的去掉,即当前元素大于等于栈顶时就出栈。
- 找左侧比它小:正遍历,找比它小就要把比它大或等的去掉,即当前元素小于等于栈顶时就出栈。
- 找右侧比它大:逆遍历,找比它大就要把比它小或等的去掉,即当前元素大于等于栈顶时就出栈。
- 找右侧比它小:逆遍历,找比它小就要把比它大或等的去掉,即当前元素小于等于栈顶时就出栈。
总代码:
cpp
#include <iostream>
#include <stack>
using namespace std;
const int N = 1e3 + 10;
int n;
int arr[N];
int ret[N]; // 存储第i个元素的解, 0表示无解
// 模板1:寻找当前元素左侧,离它最近,并且比它大的元素在哪
void test1()
{
stack<int> st; // 单调递减栈 - 存的下标
for (int i = 1; i <= n; i++)
{
while (st.size() && arr[st.top()] <= arr[i]) st.pop();
if (st.size()) ret[i] = st.top();
st.push(i);
}
// 验证一下结果
for (int i = 1; i <= n; i++) cout << ret[i] << ' ';
cout << endl;
}
// 模板2:寻找当前元素左侧,离它最近,并且⽐它⼩的元素在哪
void test2()
{
stack<int> st; // 单调递增栈 - 存的下标
for (int i = 1; i <= n; i++)
{
while (st.size() && arr[st.top()] >= arr[i]) st.pop();
if (st.size()) ret[i] = st.top();
st.push(i);
}
// 验证一下结果
for (int i = 1; i <= n; i++) cout << ret[i] << ' ';
cout << endl;
}
// 模板3:寻找当前元素右侧,离它最近,并且⽐它⼤的元素在哪
void test3()
{
stack<int> st; // 单调递减栈 - 存的下标
for (int i = n; i >= 1; i--)
{
while (st.size() && arr[st.top()] <= arr[i]) st.pop();
if (st.size()) ret[i] = st.top();
st.push(i);
}
// 验证一下结果
for (int i = 1; i <= n; i++) cout << ret[i] << ' ';
cout << endl;
}
// 模板4:寻找当前元素右侧,离它最近,并且⽐它⼩的元素在哪
void test4()
{
stack<int> st; // 单调递减栈 - 存的下标
for (int i = n; i >= 1; i--)
{
while (st.size() && arr[st.top()] >= arr[i]) st.pop();
if (st.size()) ret[i] = st.top();
st.push(i);
}
// 验证一下结果
for (int i = 1; i <= n; i++) cout << ret[i] << ' ';
cout << endl;
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++) cin >> arr[i];
test4();
return 0;
}
四、单调栈的例题
1. P5788 【模板】单调栈 - 洛谷
题目链接:P5788 【模板】单调栈 - 洛谷
问题内容:
它的意思就是让我们在一个数组中,寻找第 i 个数的右侧,第一个比它大的数的下标。
实现代码为:
cpp
#include <iostream>
#include <stack>
using namespace std;
const int N = 3e6 + 10;
int n;
int a[N]; // 原始数据
int ret[N]; // ret[i] 表示第 i 个元素右边第一个大于 a[i] 的元素的下标
int main()
{
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
// 单调栈-递减栈-存下标
stack<int> st;
for (int i = n; i >= 1; i--)
{
while (st.size() && a[st.top()] <= a[i]) st.pop(); // 找比它大,则a[i]>=栈顶就出栈
// 栈不为空就更新存在的结果
if (st.size()) ret[i] = st.top();
st.push(i);
}
// 输出结果
for (int i = 1; i <= n; i++) cout << ret[i] << ' ';
return 0;
}
2. P1901 发射站 - 洛谷
题目链接:P1901 发射站 - 洛谷
问题内容:
解题思路:因为发出的能量只被两边最近的 比它高的发射站接收,也就是说我们只要遍历一下这几个发射站,对于每一个发射站,我们需要在它的左侧和右侧找到比它高度更大的,且最近的发射站的位置(单调栈来解决) ,并让这两个位置都增加当前发射站的能量。题目要求接收最多的能量,所以最后求出它的最大之即可。
实现代码:
cpp
#include <iostream>
#include <stack>
using namespace std;
typedef long long LL; // 数据有10^9,int存不下
const int N = 1e6 + 10;
LL n;
LL h[N], v[N]; // 高度和能量
LL sum[N]; // 第i个发射站的最终总能量
int main()
{
cin >> n;
for (int i = 1; i <= n; i++) cin >> h[i] >> v[i];
// 先找左侧比它大
stack<LL> st;
for (int i = 1; i <= n; i++)
{
while (st.size() && h[st.top()] <= h[i]) st.pop(); // 当前元素>=栈顶时出栈
if (st.size()) sum[st.top()] += v[i]; // 栈不为空,则栈顶存的就是左侧比它大的位置下标
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()) sum[st.top()] += v[i]; // 栈不为空,则栈顶存的就是右侧比它大的位置下标
st.push(i);
}
// 计算最大值
LL ret = 0;
for (int i = 1; i <= n; i++) ret = max(ret, sum[i]);
cout << ret << endl;
return 0;
}
3. SP1805 HISTOGRA - 直方图中的最大矩形
题目链接:SP1805 HISTOGRA - Largest Rectangle in a Histogram - 洛谷
问题内容:
题目解析:这个题目的意思是让我们在一条水平线上的 n 个宽为 1 的矩形中,画出一个矩形,这个矩形的各个部分都必须包含在水平线上的 n 个宽为 1 的矩形组成的整体中。
解题方法:
解法1:扫描线。通过一条线,从上到下,来扫描歌词通过这条线分隔出现的矩形,再在其中求最大面积。(实现比较复杂,暂时不说,本次主要是要通过单调栈的方法来解决)如图所示;
解法2:单调栈。针对每一根柱子,找到它向左和向右所能达到的最远位置,然后中间区域就是我们找到的以当前柱子能找到的最大矩形。那么我们以每一个柱子都找一遍这样的矩形,再求一个面积最大值,得到的就是要求的矩形面积。
所以此时问题就转化成了:遍历所有矩形,对每一个矩形,都向两边找一下最近的比当前高度更小的位置(假设左边位置为x,右边位置为y),则面积就是((y-1) - (x+1))*h i 。
此时的找左侧和右侧的最近的比当前高度更小的位置就是通过单调栈解决的。
细节:如果没有找到比当前高度低的矩形,则可以将其边界位置设在最左边和最右边。对应左侧,则设为0即可(必须要保证下标从1开始才行);右侧则设为 n+1 。
实现代码为:
cpp
#include <iostream>
#include <stack>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
LL n;
LL h[N];
LL x[N], y[N]; // x[i]表示左侧比第i个矩形高度小的位置,y[i]表示右侧比第i个矩形高度小的位置
int main()
{
while (cin >> n, n) // 多组数据
{
for (int i = 1; i <= n; i++) cin >> h[i];
// 找左端点
stack<int> st; // 存下标
for (int i = 1; i <= n; i++)
{
while (st.size() && h[st.top()] >= h[i]) st.pop(); // 比当前h大的栈顶出栈
if (st.size()) x[i] = st.top();
else x[i] = 0; // 没有比h[i]低的矩形,边界就是第1个矩形之前的位置
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(); // 比当前h大的栈顶出栈
if (st.size()) y[i] = st.top();
else y[i] = n + 1; // 没有比h[i]低的矩形,右边界就是第n个矩形之后的位置
st.push(i);
}
// 计算面积
LL ret = 0;
for (int i = 1; i <= n; i++)
{
ret = max(ret, (y[i] - x[i] - 1) * h[i]);
}
cout << ret << endl;
}
return 0;
}
感谢各位观看!希望大家多多支持!