单调栈
1.什么是单调栈?
从名字上就听的出来,单调栈中存放的数据应该是有序的,所以单调栈也分为单调递增栈和单调递减栈,故其分为两种
单调递增栈:单调递增栈就是从栈底到栈顶数据是从大到小
单调递减栈:单调递减栈就是从栈底到栈顶数据是从小到大
从上面的定义可以知道它就是一个栈,只不过里面存储的元素是有顺序的, 这里我们使用数组模拟栈:
#include<iostream>
#include<string>
using namespace std;
constexpr int N = 1e+5 + 10;
int st[N], tt, n;
// 这里的栈是满堆栈写法, 堆栈指针指向最后压入栈的有效数据项,此时堆栈入栈操作要先调整指针再写入数据,故这里是前置++
auto push(int x) { st[++tt] = x; } // 入栈
auto pop() {--tt;} // 出栈
auto empty() -> bool { return tt == 0;} // 判断是否为空, 初始值 tt = 0;表示为空
auto top() -> int { return st[tt]; } // 返回栈顶元素
单调栈的题型相对来说比较固定,接下来分析几道相关题目来具体了解单调栈的作用 这里主要看单调栈的应用,因为其本身就是一个栈,在数据结构上没什么不一样的,关键是看其应用场景下如何实现元素的顺序存储.
相关练习链接 : AcWing 单调栈练习
#include<iostream>
using namespace std;
constexpr int N = 1e+5 + 10;
int st[N], tt, n;
auto push(int x) { st[++tt] = x; } // 入栈
auto pop() {--tt;} // 出栈
auto empty() -> bool { return tt == 0;} // 判断是否为空, 初始值 tt = 0;表示为空
auto top() -> int { return st[tt]; } // 返回栈顶元素
auto main() -> int
{
cin >> n;
for(int i = 1; i <= n; ++i)
{
int x; cin >> x;
while(!empty() && top() >= x) pop(); // 先判断是否为空, 若不为空切栈顶元素大于等于目前比较的数, 则出栈栈顶元素,直到栈顶元素为符合条件
if(!empty()) cout << top() << " ";
else cout << "-1 ";
push(x); // 此时再插入x,x此时为栈顶,是栈顶最大的元素
}
cout << endl;
return 0;
}
更简便的代码
#include<iostream>
using namespace std;
constexpr int N = 1e+5 + 10;
int st[N], tt;
auto main() -> int
{
int n; cin >> n;
while(n--)
{
int a; cin >> a;
while(tt && a <= st[tt]) --tt;
if(!tt) cout << -1 << " ";
else cout << st[tt] << " ";
st[++tt] = a;
}
}
思路分析 : 例如这样一组数据 2 3 5 4 x 现在找x左边离它最近且比它小的那个数,首先第一个条件是离它最近,故这里考虑用一下朴素做法实现我们可以直接用栈,将前面的元素依次进栈,此时栈顶元素就是左边离它最近的元素,若该元素大于等于x,遍历下一个元素比较直到找到符合条件的数,在这里我们发现5这个数子无论如何都不会作为一个正解,因为其后面有个4, 故5永远都不会是一个可选的答案,故我们可以直接将这个数字直接出栈,而不用每次都遍历一遍。 这样每一个数字最多的操作就是入栈一次和出栈一次,时间复杂度为 O(n), 而朴素做法为O(n2);
相关练习链接 : AcWing 131. 直方图中最大的矩形
1.确定基本思路 :
如上图所示,直方图由若干高度不同,宽度均为1的矩形组成,想要找出直方图中面积最大的矩形, 我们只要看每一个矩形其向左能扩展多少,向右能扩展多少, 以第一个矩形为例,其向左无法扩展, 向右由于2号矩形的高度小于1号矩形,故向右也无法扩展,故以第一个矩形向左向右扩展的最大矩形面积即为 h1 * 1, 由于2号矩形其高度较低,故其向左能扩展到1, 向右能扩展到7号矩形, 故以2号矩形向右向左扩展的最大面积为 h2 * 7 , 以此类推, 最后得出的是以4号矩形向左向右扩展得到的矩形面积最大。 故向左扩展向右扩展的依据就是其高度不能小于自身高度。 故一个朴素做法就是遍历每一个高度的矩形,然后向左遍历直到遇到一个高度小于目前所要扩展的矩形的高度, 向右同理。 这种朴素做法最坏的时间复杂度为 o(n2), 这里数据的个数范围 1 ~ 100000 , 故会超时 。
2.单调栈进行优化
跟上面的类似, 这里以向左扩展为例,上面基本思路已经分析了,向左扩展的终止条件就是找到一个高度小于自己的矩形, 以上面为例,当需要找到一个矩形左边高度比它小的矩形,这时遍历的过程中像2号矩形和3号矩形就不需要进行遍历,因为4号矩形的高度比2和3都要低,且在2和3的右边, 故2和3绝对不会作为一个正解,故此时我们并不需要遍历2和3,即我们用栈并不需要存储2号和3号。
#include<iostream>
using namespace std;
constexpr int N = 1e+5 + 10;
long long h[N], l[N], r[N], st[N], tt, n; // h保存高度, l数组保存每个矩形向左扩展到的位置, r数组表示没给矩形向右扩展到的位置 st入栈的是每个矩形的位置
auto main() -> int
{
while(cin >> n, n) // 题目说明有n组测试数据, 当 n 为 0 时退出
{
h[0] = h[n + 1] = -1; // 定义左右边界 0 和 n + 1 其高度为 -1, 直方图中矩形的高度范围为 0 ~ 1000000000
for(int i = 1; i <= n; ++i) cin >> h[i];
tt = 0; // 满堆栈写法
st[tt] = 0; // 栈顶初始化为直方图的左边界
// 向左扩展
for(int i = 1; i <= n; ++i)
{
while(h[i] <= h[st[tt]]) --tt; // 若当前高度小于栈顶高度向左扩展,栈顶元素弹出
l[i] = st[tt]; // 此时栈顶的高度为小于当前矩形的高度
st[++tt] = i; // 将当前矩形的位置入栈
}
// 同理 向右扩展
tt = 0;
st[tt] = n + 1;
for(int i = n; i ; --i) // 从右往左遍历
{
while(h[i] <= h[st[tt]]) --tt;
r[i] = st[tt];
st[++tt] = i;
}
long long res = -1;
// 遍历每个矩形,计算出其向左向右扩展后的矩形最大值.
for(int i = 1; i <= n; ++i) res = max(res, h[i] * (r[i] - l[i] - 1));
cout << res << endl;
}
return 0;
}