单调栈

XyzL

2020-10-19 10:29:58

Personal

## 前置知识: 1:栈的运用 ## 何为单调栈? 从字面意思上来看,无非就是有某种**单调性**的栈。 一般解释,就是栈内**所有**元素按照所维护的值**递增/递减**顺序排列的栈。 ## 单调栈有何用? 单调栈分两种,一种是单调**递增**栈,一种是单调**递减**栈。一般来说,单调栈解决的问题是求某个元素下一个比其**大/小**的元素。 所以,当我们解决比较前后元素的**大小**关系时,我们就可以使用单调栈。 ## 如何解决? 此处借鉴天天~的奇思妙想使读者更好地理解... 我们可以先模拟一下单调栈的过程。 假如有一天~~修仙~~武林高手石破天要传授神功《太玄经》,各路武林高手都想学,于是就到石破天家门口排队,但石破天大侠并不喜欢来的晚的人,所以就按照人来的先后顺序教,先来的学的武功就高深,来的越靠后学的就越差,但是可以保证只要来学武功的人都能学到。 ![武林秘籍](https://cdn.luogu.com.cn/upload/image_hosting/kl56fn6f.png) 当然,即使是内功高手扫地僧也想要这么一副神功,所以任何人当然是想学的越高深越好。所以每当有武功高的人就会找到前面武功稍逊的人说:“你别去学了,我这里有本武功更适合你。” 武功稍逊的那一个一听就答应了 ~~(就算不答应也打不过啊...)~~,从其哪里学了一门武术高高兴兴的走了,这样武功高的那个就排在了前面。 下面我们来从前往后模拟一下过程: 1. 首先来的是炮灰甲,炮灰甲一看,OK,栈内没有人就可以先排在第一位 2. 然后扫地僧来了,扫地僧教给了炮灰甲自己的功夫,然后扫地僧让炮灰甲离开自己排在第一位 3. 然后杨过过来,看到前面是扫地僧,自己打不过只能老老实实站到队里 4. 后面一直来人,但都打不过前面的人,直到张三,(队列:扫地僧 杨过 慕容复 张三) 5. 然后张无忌来了,他首先看到前面是张三,张无忌教给张三自己的功夫并让他离开),然后张无忌再往前看到了慕容复(过程如上所示),直到遇到扫地僧,OK,自己打不过,老老实实的占到了后面,(杨过,慕容复,张三的师傅为张无忌)。(队列:扫地僧,张无忌) 6. 柯镇恶来了,自己打不过扫地僧,只能站在后面。 7. 然后乔峰来了把柯镇恶和张无忌打发走。(队列:扫地僧,乔峰) 8. 然后李四来了,打不过乔峰,只能站到了最后。 9. 最后扫地僧,乔峰,李四成功学到了~~修仙~~武林秘籍《太玄经》。 武功对应: ![武功对应](https://img-blog.csdnimg.cn/20190402213134882.png) 这样我们就模拟完了一个**单调递减栈**的过程,由于每个元素只会留在栈中或被弹走一次,所以时间复杂度为 $O(n)$ 。 ```cpp while (!stk.empty() && a[i] <= a[stk[top]]) { // 打得过 a[stk[top]] = i; // 教授武功 top--; // 赶走 } stk[++top] = i; // 入栈 ``` Tip: 栈中存的不是**值**,而是**下标**。 ------------ ## 如何应用? [LG2422 良好的感觉](https://www.luogu.com.cn/problem/P2422) ### 题意: 给定一个长度为 $n(1≤n≤100,000)$ 的正整数序列 $P$ ,求任意区间中,**区间最小值 × 区间和**的最大值。 ### 分析: 对于每个区间我们可以用朴素的做法暴力枚举一遍,然后再求每个区间的**区间最小值**和**区间和**,时间复杂度为 $O(n ^ 3)$ 。 在此之上我们可以用**前缀和**预处理,这要我们求每个区间和的时间复杂度只有 $O(1)$ 。 现在我们可以利用一个**贪心**的思想,如果在一个正整数序列任意区间内,区间最小值 $P_i$ 都是**确定**的,**区间长度越长,那么对于答案就更优**。 所以说我们可以去维护每一个 $P_i$ 所在的所有区间中,满足在此区间中 $P_i$ 最小的区间的**左右下标**,再利用**前缀和**,求出以每一个 $P_i$ 当区间最小值的 $Ans$ 中取最大值。 这时候我们就可以用单调栈来维护。 ```cpp #include<bits/stdc++.h> using namespace std; inline int read() { int x = 0, f = 1; char c = getchar(); while (c < '0' || c > '9') { if(c == '-') { f = -1; } c = getchar(); } while (c <= '9' && c >= '0') { x = (x << 1) + (x << 3) + (c ^ 48), c = getchar(); } return x * f; } const int Maxn = 2e5 + 5; int n, top, a[Maxn], stk[Maxn], L[Maxn], R[Maxn]; long long ans = 0, sum[Maxn]; int main() { n = read(); for (register int i = 1; i <= n; ++i) { a[i] = read(); sum[i] = a[i] + sum[i - 1]; // 预处理前缀和 } top = 0; // 用数组模拟栈,初始化栈顶 for (register int i = 1; i <= n; ++i) { while (top > 0 && a[i] <= a[stk[top]]) { // 如果此时栈顶比当前 a[i] 要小 R[stk[top--]] = i; // 记录 R数组 ,并将栈顶弹出 } stk[++top] = i; // 插入栈顶 } for (register int i = 1; i <= n; ++i) { // 记录剩下栈中元素,他们的右边没有比他们小的 if(R[i] == 0) { R[i] = n + 1; // 边界 } } top = 0; for (register int i = n; i >= 1; --i) { // L数组 记录过程同 R数组 while (top > 0 && a[i] <= a[stk[top]]) { L[stk[top--]] = i; } stk[++top] = i; } for (register int i = 1; i <= n; ++i) { ans = max(ans, (sum[R[i] - 1] - sum[L[i]]) * a[i]); // 对于每个 a[i] 求其对于的 Ans,取最大值 } printf("%lld\n", ans); return 0; } ``` 另外,我们还有一种做法。 可以考虑,对于此序列中的最大值,在没有重复的情况下,他的 $L$ 和 $R$ 都只是它自己的下标,而对于第二大值,除了最大值和重复, $L$ 和 $R$ 都只是它自己的下标,如果有了最大值和重复,我们就可以去**继承**最大值和重复所对应的 $L$ 和 $R$ ,并且我们可以用**并查集**去维护。 [POI2008 PLA-Postering](https://www.luogu.com.cn/problem/P3467) ### 题意: 给定 $n$ 个海报,排成一排。现在希望用尽量少的矩形海报 $Cover$ 住它们,且不能超出边界。 ### 分析: **所有的形状,无非就是一个凹字形或一个凸字形排列起来的。** ![凹](https://cdn.luogu.com.cn/upload/image_hosting/cywb4oju.png) 假如成“凹”字形: 那么那一块空白会把旁边两块海报隔开,还是需要**三块海报**。 ![凸](https://cdn.luogu.com.cn/upload/image_hosting/igsxvbv7.png) 假如成“凸”字形: 我们只需要**两块海报**。 所以我们到这里可以想到维护一个**单调递增**的栈,如果遇到一个新的建筑如果比上一个矮,就删掉上一个海报,**直到上一块海报的值小于它**。 如果删除过程中有一块海报与其**相等**,那么我们就只要用**一块海报**覆盖掉,这块海报在之前就被计算过了,那就比原先的海报少一块。 否则,答案就不用变,因为**没有可以节省的海报**。 宽度无用... ### 代码: ```cpp #include<bits/stdc++.h> using namespace std; inline int read() { int x = 0, f = 1; char c = getchar(); while (c < '0' || c > '9') { if(c == '-') { f = -1; } c = getchar(); } while (c <= '9' && c >= '0') { x = (x << 1) + (x << 3) + (c ^ 48), c = getchar(); } return x * f; } const int Maxn = 250052; int n, ans, top, h[Maxn], s[Maxn]; int main() { n = ans = read(); int di; // 宽度无用 for (register int i = 1; i <= n; ++i) { di = read(), h[i] = read(); while (s[top] > 0 && h[i] <= h[s[top]]) { // 比较栈顶 if(h[i] == h[s[top]]) { // 如果相等 ans--; // 节约一张海报 } top--; } s[++top] = i; } printf("%d\n", ans); return 0; } ``` [CF1299C Water Balance](https://codeforces.com/contest/1299/problem/C) ### 题意: 给定一个长度为 $n(1≤n≤1,000,000)$ 的正整数序列 $P$ ,有一操作可以使**任意区间**的数,都变成此区间的**平均数**。 最后能得到的**字典序最小**的结果(即满足越前面的数越小越好)。 ### 分析: 通过观察,可以发现,答案序列中的数都是**单调不下降**的。因为如果出现下降字段的话,那么我们可以继续操作使得答案更优。 反之,如果想得到答案,我们就**必须**是序列不下降;并且如果序列不下降,那么答案字典序就最小。 故,结论:**只要通过操作使整个数列不下降,那么它就是答案。** 所以我们可以使用**单调栈**维护每个取过平均数的区间,每次新加进来一个数的时,如果它能使栈顶的区间平均数更小,那么就可以利用**贪心**的思想,将它与这个区间**合并**。合并之后,再与栈顶对应的区间能不能使栈中在它下面的区间平均数**更小**,再进行合并。 如此反复,直到单调栈中的元素**单调递增**(不相等是因为平均数相等的区间可以直接合并)。由于上面的结论可知,这种方法能得到最终答案。 那么如何判断区间能否合并呢?其实我们只需要**比较平均数**即可,后面的区间如果比前面的区间平均数小,那么就可以合并。对于计算新区间的平均数,只需要维护两个区间的**左端点,右端点和平均数**,将其求和后再求平均数即可。 最后的分段结果保存在栈中,由于栈顶保存的是最后一个区间,所以需要递归倒序输出。 借用于[一只大头](https://www.luogu.com.cn/blog/lcm2019/solution-cf1299c)的题解,欢迎去膜拜评论。 ### 代码: ```cpp #include<bits/stdc++.h> using namespace std; inline int read() { int x = 0, f = 1; char c = getchar(); while (c < '0' || c > '9') { if (c == '-') { f = -1; } c = getchar(); } while (c <= '9' && c >= '0') { x = (x << 1) + (x << 3) + (c ^ 48), c = getchar(); } return x * f; } const int MaxN = 1000010; int n; double a[MaxN]; struct Node { int l, r; double val; Node () { l = r = val = 0; } Node (int l = 0, int r = 0, double val = 0) : l(l), r(r), val(val) { } }; inline double Calc (Node a) { return a.val * (a.r - a.l + 1); } stack <Node> s; inline void Update (int x) { Node p = Node(s.top().l, x, (Calc(s.top()) + a[x]) / (x - s.top().l + 1)); s.pop(); while (s.empty() == false && p.val < s.top().val) { Node cur = s.top(); s.pop(); p.val = (Calc(p) + Calc(cur)) / (p.r - cur.l + 1); p.l = cur.l; } s.push(p); return ; } inline void Put () { if (s.empty() == true) { return ; } Node tmp = s.top(); s.pop(); Put(); for (register int i = tmp.l; i <= tmp.r; i++) { printf("%.9lf\n", tmp.val); } return ; } int main() { n = read(); for (register int i = 1; i <= n; ++i) { a[i] = read(); } for (register int i = 1; i <= n; ++i) { if (s.empty() == true || a[i] > s.top().val) { s.push(Node(i, i, a[i])); } else { Update(i); } } Put(); return 0; } ``` [POI2008 PLA-Postering](https://www.luogu.com.cn/problem/P3467) ### 题意: 给定一个长度为 $n(1≤n≤200,000)$ 的正整数序列 $P$ ,求长度从 $1$ 到 $n$ 的区间中,所有**区间中最小值**的最大值。 ### 分析: 我们可以发现其与[LG2422 良好的感觉](https://www.luogu.com.cn/problem/P2422)有着一样的地方,我们同样可以维护每个点的**左右边界**,最后再将输出修改一下即可。 ### 代码: ```cpp #include<bits/stdc++.h> using namespace std; inline int read() { int x = 0, f = 1; char c = getchar(); while (c < '0' || c > '9') { if(c == '-') { f = -1; } c = getchar(); } while (c <= '9' && c >= '0') { x = (x << 1) + (x << 3) + (c ^ 48), c = getchar(); } return x * f; } const int Maxn = 2e5 + 5; int n, top, a[Maxn], stk[Maxn], L[Maxn], R[Maxn], res[Maxn]; int main() { n = read(); for (register int i = 1; i <= n; ++i) { a[i] = read(); } top = 0; for (register int i = 1; i <= n; ++i) { while (top > 0 && a[i] < a[stk[top]]) { R[stk[top--]] = i - 1; } stk[++top] = i; } for (register int i = 1; i <= n; ++i) { if(R[i] == 0) { R[i] = n; } } top = 0; for (register int i = n; i >= 1; --i) { while (top > 0 && a[i] < a[stk[top]]) { L[stk[top--]] = i + 1; } stk[++top] = i; } for (register int i = 1; i <= n; ++i) { if(L[i] == 0) { L[i] = 1; } } for (register int i = 1; i <= n; ++i) { res[R[i] - L[i] + 1] = max(res[R[i] - L[i] + 1], a[i]); } for (register int i = n; i >= 1; --i) { res[i] = max(res[i], res[i + 1]); } for (register int i = 1; i <= n; ++i) { printf("%d ", res[i]); } puts(""); return 0; } ``` [CF817D Imbalanced Array ](http://codeforces.com/contest/817/problem/D) ### 题意: 给定一个长度为 $n(1≤n≤200,000)$ 的序列 $P$。对于给定由 $n$ 个元素构成的序列,一个子数组的不平衡值是这个区间的最大值与最小值的差值。数组的不平衡值是它所有子数组的不平衡值的总和。 ### 分析: 此题题面翻译的有些复杂,简而言之,就是求所有连续子序列中,**最大值 $-$ 最小值 的总和**。 所以我们其实只根据**乘法原理**来求出 最大 $=$ $\sum_{i=1}^n$ $a[i] * (i - L_{max}[i]) * (R_{max}[i] - i)$ 最小 $=$ $\sum_{i=1}^n$ $a[i] * (i - L_{min}[i]) * (R_{min}[i] - i)$ 然后相减即可。 ### 代码: ```cpp #include <bits/stdc++.h> using namespace std; inline int read() { int x = 0, f = 1; char c = getchar(); while (c < '0' || c > '9') { if (c == '-') { f = -1; } c = getchar(); } while (c <= '9' && c >= '0') { x = (x << 1) + (x << 3) + (c ^ 48), c = getchar(); } return x * f; } const int INF = ~(1 << 31); const int MaxN = 1e6 + 5; long long n, top, ans = 0, a[MaxN], stk[MaxN], Max_L[MaxN], Max_R[MaxN], Min_L[MaxN], Min_R[MaxN]; int main() { n = read(); for (register int i = 1; i <= n; ++i) { a[i] = read(); } a[0] = INF; top = 1; stk[1] = 0; for (register int i = 1; i <= n; ++i) { while (a[i] >= a[stk[top]]) { top--; } Max_L[i] = stk[top]; stk[++top] = i; } a[n + 1] = INF; top = 1; stk[1] = n + 1; for (register int i = n; i >= 1; --i) { while (a[i] > a[stk[top]]) { top--; } Max_R[i] = stk[top]; stk[++top] = i; } a[0] = -2147483647; top = 1; stk[1] = 0; for (register int i = 1; i <= n; ++i) { ans += a[i] * (i - Max_L[i]) * (Max_R[i] - i); // 最大值 } for (register int i = 1; i <= n; ++i) { while (a[i] <= a[stk[top]]) { top--; } Min_L[i] = stk[top]; stk[++top] = i; } a[n + 1] = -2147483647; top = 1; stk[1] = n + 1; for (register int i = n; i >= 1; --i) { while (a[i] < a[stk[top]]) { top--; } Min_R[i] = stk[top]; stk[++top] = i; } for (register int i = 1; i <= n; ++i) { ans -= a[i] * (i - Min_L[i]) * (Min_R[i] - i); // 最小值 } printf("%lld\n", ans); return 0; } ``` 课后作业:CF5E Bindian Signalizing ## 总结: 1. 单调栈,可以简单理解为不会从队头出只会从队尾出的[单调队列](https://xyzl.blog.luogu.org/DQ-OP-DP),**但只能从队尾取出。** 2. 单调栈一般所维护的是元素前后**大小**的关系。 3. 由于单调栈个元素同单调队列一样只进出一次,所以基本复杂度是**线性**的。