单调栈
概述
栈中元素满足单调性的线性数据结构,单调栈一般维护的是一个数前/后第一个大于/小于他的数。
- 单调栈解决的主要问题是什么呢?
就跟单调队列差不多。单调队列主要处理的是一个区间内的最大/小值,而单调栈处理的是寻找以某个值为最小/大值的最大区间。相比较,实际上单调栈用的虽然少一些,但是比单调队列更加灵活多变。
- 为什么单调栈是正确的呢?
对于这道题来说,我们定义一个元素失效,当且仅当这个元素的 \(f_i\) 被保存。我们假设将栈中已有元素。既然栈中元素没有被弹出,那么证明还没有遇到比它大的元素。当我们的元素从栈中弹出的时候,这证明了它发现了第一个比它还要大的数,这道题刚好满足,于是保存 \(f_i\) ,继续算法流程。同理,对于之前的主要问题,我们找到了一个比它还要大的数,说明这个区间结束了。
- 单调栈的时间复杂度是?
想一下,我们的每一个元素最多进栈/出栈一次,所以说时间复杂度 O(n)。
-
结束标识符
对于某些特殊题目,栈内元素出不完,会导致答案错误,这时候我们要添加结束标识符强制吐出所有栈内元素。
template <class T, class Cmp = std::greater_equal<T>> // >=
class MonotonyStack
: private std::stack<std::pair<T, int>, std::vector<std::pair<T, int>>> {
static constexpr Cmp cmp = Cmp();
public:
MonotonyStack(const T &val, int id) {//一开始要放入的极限元素
this->emplace(val, id);
}
int push(const T &val, int id) {
while (this->empty() == false and cmp(this->top().first, val)) {
this->pop();
}
assert(this->empty() == false);
int result = this->top().second;
this->emplace(val, id);
return result;
}
};
板题
题意
即求每一个数的下一个更大数的下标。
思路
- 维护一个单调递减的栈,即栈顶永远是大于等于目前元素的最小值
- 每次插入元素与栈顶进行比较
- 大于等于栈顶
- 栈顶不停出栈,直到该元素小于栈顶或栈为空。
- 出栈的每一个栈顶,对应的答案都是该元素。
- 小于栈顶
- 入栈
- 大于等于栈顶
时间复杂度
从出栈入栈的角度切入分析,所有元素都只会出栈一次,入栈一次,所以复杂度就是 \(\operatorname O(n)\)
代码
template <class T, class Cmp = std::greater_equal<T>> // >=
class MonotonyStack
: private std::stack<std::pair<T, int>, std::vector<std::pair<T, int>>> {
static constexpr Cmp cmp = Cmp();
public:
MonotonyStack(const T &val, int id) {
this->emplace(val, id);
}
int push(const T &val, int id) {
while (this->empty() == false and cmp(this->top().first, val)) {
this->pop();
}
assert(this->empty() == false);
int result = this->top().second;
this->emplace(val, id);
return result;
}
};
void solve()
{
// #define tests
int n; std::cin >> n;
std::vector<int> a(n); for (auto& ai : a) {std::cin >> ai;}
MonotonyStack<int, std::less_equal<int>> monostack(1E9, -1);
std::vector<int> res(n);
for (int i = n - 1; i >= 0; i--) {
res[i] = monostack.push(a[i], i);
}
for (auto& x : res) {std::cout << x + 1 << ' ';} std::cout << '\n';
}
最大矩形面积
D-Largest Rectangle in a Histogram_0x11 基本数据结构-栈 (nowcoder.com)
思路
考虑维护每一个元素 \(i\) 左端第一个比它小的下标 \(l_i\) 及右端第一个比它小的下标 \(r_i\)。
显然可能的面积就是 \(a_i \times (r_i - l_i - 1)\)
代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
while(cin >> n && n) {
vector<int> a(n + 2);
vector<int> l(n + 2), r(n + 2);
for (int i = 1; i <= n; i++) cin >> a[i];
a[n + 1] = 0;
long long ans = 0;
stack<int> st;
for (int i = 1; i <= n; i++) {
while(!st.empty() && a[i] <= a[st.top()]) {
st.pop();
}
if (!st.empty()) l[i] = st.top();
else l[i] = 0;
st.push(i);
}
while(!st.empty()) st.pop();
for (int i = n; i; i--) {
while(!st.empty() && a[i] <= a[st.top()]) {
st.pop();
}
if (!st.empty()) r[i] = st.top();
else r[i] = n + 1;
st.push(i);
}
for (int i = 1; i <= n; i++) {
ans = max(ans, 1LL * a[i] * (r[i] - l[i] - 1));
}
cout << ans << endl;
}
return 0;
}
CF1730C
题意
给定一个仅包含 \(0\) 到 \(9\) 这几个数字的字符串,你可以执行以下操作任意次:
选择字符串中的一个数字 \(d\),将 \(d\) 删除后,向字符串任意位置插入一个数字 \(\min(d+1,9)\)
求能够得到的字典序最小的字符串。
思路
想要让字典序最小,就要尽可能将小的数放在前面。每次执行一次操作后,我们都会让一个数变大,只有变大后把它放在字符串后面,让一个更小的数能到前面来,他才能更优。
维护一个单调栈,将弹出的元素 \(d=\min(d+1,9)\) 全部存入一个 vector
中,将所有元素排序。最后输出答案,在输出单调栈中元素的同时,输出 vector
中与之相等的元素,因为已经排完序了,直接从前向后扫即可。
void solve() {
string s;
stack<char> st;
vector<char> els,ans;
cin >> s;
int len = s.length();
for(int i = 0;i < len;i++) {
while(!st.empty() && st.top() > s[i]) {
els.push_back(min((char)(st.top() + 1),'9'));
st.pop();
}
st.push(s[i]);
}
while(!st.empty()) {
ans.push_back(st.top());
st.pop();
}
sort(els.begin(),els.end());
int pos = 0;
for(int i = ans.size() - 1;i >= 0;i--) {
cout << ans[i];
while(pos < els.size() && els[pos] >= ans[i]
&& els[pos] <= ans[i-1] && i > 0) cout << els[pos++];
}
for(int i = pos;i < els.size();i++) cout << els[i];
cout << endl;
return;
}