单调栈学习笔记
先通过一道题目来引入这个数据结构
例题 1 :
Problem 1 题目描述
给定一个长度为 \(n\) 的数列 \(\{a\}\) ,求出数列的每一个数的左边的离它最近的比它小的数,若不存在输出 \(0\) 。
\(1 \leq n \leq 3 \times 10^6 ,1 \leq a_i \leq 10^9\) 。
Solution
考虑朴素做法:对于每个 \(i\) ,让 \(j\) 从 \(i-1 \to 1\) 循环,如果 \(a_j < a_i\) ,那么就输出 \(j\) 。
时间复杂度是 \(\mathcal{O}(n^2)\) ,考虑怎么优化:
设 \(a_j > q_k\) 且 \(j < k\) ,那么对于 \(k\) 以及 \(k\) 右边的所有数,答案都不可能是 \(j\) 。
证明:如果 \(\exist i \in [1,n], a_i > a_j\) ,那么一定有 \(a_i > a_k\) ,因为 \(k>j\) ,所以 \(k\) 和 \(i\) 的距离一定小鱼 \(j\) 和 \(i\) 的距离,所以 \(j\) 不可能成为答案。
证毕。
所以我们可以维护一个栈,从左向右扫,设当前扫到的位置是 \(i\) 。
首先把栈中的所有比 \(a_i\) 大的数字删掉,因为这些数不可能成为答案。删完之后,如果栈是空的,那么答案是 \(0\) ,否则答案是栈顶元素,最后再把 \(a_i\) 压入栈。
时间复杂度是 \(\mathcal{O}(n)\) ,这是因为每个数字只会进栈一次,出栈一次。
代码如下:
#include <cstdio>
#include <cstring>
#include <cctype>
#include <algorithm>
#include <iostream>
using namespace std;
inline int read() {
int num = 0 ,f = 1; char c = getchar();
while (!isdigit(c)) f = c == '-' ? -1 : f ,c = getchar();
while (isdigit(c)) num = (num << 1) + (num << 3) + (c ^ 48) ,c = getchar();
return num * f;
}
const int N = 1e5 + 5;
int a[N] ,s[N] ,n ,top;
signed main() {
n = read();
for (int i = 1; i <= n; i++) a[i] = read();
s[++top] = 0;
for (int i = 1; i <= n; i++) {
while (top && a[s[top]] >= a[i]) top--;
printf("%d%c" ,s[top] == 0 ? -1 : a[s[top]] ," \n"[i == n]);
s[++top] = i;
}
return 0;
}
所以单调栈最大的作用就是找到一个数的左/右边距离它最近的比它大/小的数字。
对于单调栈的应用,就是转化为上面说的作用。
例题 2 :
Problem 2 题目描述
给定一个长度为 \(n\) 的数列 \(\{a\}\) ,请你选出一个区间 \([l,r]\) ,使得:
最大,你需要输出这个最大值,以及你选出的区间的左、右端点。
\(1 \leq n \leq 10^6 ,1 \leq a_i \leq 10^6\) 。
Solution
注意到第二个因数一定是 \(\{a\}\) 中的一个数,所以我们可以枚举这样一个数。
又因为所有元素都是正数,那么区间越长,求和的答案也就越大,所以如果设当前的位置是 \(k\) ,我们要在满足 \(\min_{i=l}^r a_i = a_k\) 的条件下让 \(r-l+1\) 的值最小。
再对问题进行转化:可以找到 \(a_k\) 左边第一个比它小的位置 \(q_k\) ,右边第一个比它小的位置 \(p_k\) ,那么 \([q_k+1,p_k-1]\) 这个区间内的最小值就是 \(a_k\) 。
而上面这个问题可以使用单调栈来解决,时间复杂度是 \(\mathcal{O}(n)\) 的。
至于如何快速求区间和,用前缀和就可以。
在代码实现中,可以用一次循环同时求出所有的 \(q\) 和 \(p\) 的值,具体见代码:
#include <cstdio>
#include <cstring>
#include <cctype>
#include <algorithm>
#include <iostream>
#include <queue>
typedef long long LL;
using namespace std;
inline int read() {
int num = 0 ,f = 1; char c = getchar();
while (!isdigit(c)) f = c == '-' ? -1 : f ,c = getchar();
while (isdigit(c)) num = (num << 1) + (num << 3) + (c ^ 48) ,c = getchar();
return num * f;
}
const int N = 1e5 + 5;
int a[N] ,n; LL sum[N]; int l[N] ,r[N] ,s[N] ,top; LL ans;
signed main() {
n = read();
for (int i = 1; i <= n; i++) a[i] = read();
for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + a[i];
for (int i = 1; i <= n; i++) {
while (top && a[s[top]] > a[i]) r[s[top--]] = i; //当前的栈顶第一次被弹出,那么也就意味着栈顶到 i - 1 里没有比栈顶元素更小的值,所以栈顶的右边的最近的比它小的位置是 i
l[i] = s[top]; //把左边的所有比 i 大的数字都弹掉以后,当前栈顶就是左边第一个比它小的数(如果没有就是 s[0] = 0)
s[++top] = i;
}
while (top) r[s[top--]] = n + 1;
int nowl ,nowr;
for (int i = 1; i <= n; i++) {
LL now = (LL)(sum[r[i] - 1] - sum[l[i]]) * a[i];
if (now > ans) ans = now ,nowl = l[i] + 1 ,nowr = r[i] - 1;
}
printf("%lld\n%d %d\n" ,ans ,nowl ,nowr);
return 0;
}
Problem 3 题目描述:
给出 \(n\) ( \(1 \leq n \leq 10^5\) )个宽度为 \(1\) 的矩形,垂直拼接于一水平线上(如下图)。告诉每个矩形的高 \(h_i\) ( \(0 \leq h_i \leq 10^9\) ),求出图形中最大的矩形面积,如这个直方图的最大面积是阴影部分:
Solution
因为选出的矩形不能超出原来的范围,所以如果我们要在 \([l,r]\) 这个范围内选出一个合法的最大的矩形,它的面积就是 \((r-l+1) \times \min_{i=l}^r h_i\) 。
对于第二部分,可以使用上一个例题同样的方法维护。
时间复杂度是 \(\mathcal{O}(n)\) 的,代码如下:
#include <cstdio>
#include <cstring>
#include <cctype>
#include <algorithm>
#include <iostream>
#include <queue>
typedef long long LL;
using namespace std;
inline int read() {
int num = 0 ,f = 1; char c = getchar();
while (!isdigit(c)) f = c == '-' ? -1 : f ,c = getchar();
while (isdigit(c)) num = (num << 1) + (num << 3) + (c ^ 48) ,c = getchar();
return num * f;
}
const int N = 1e5 + 5 ,INF = 0x3f3f3f3f;
int a[N] ,l[N] ,r[N] ,n ,s[N] ,top;
signed main() {
while (n = read()) {
for (int i = 1; i <= n; i++) a[i] = read();
for (int i = 1; i <= n; i++) {
while (top && a[s[top]] > a[i]) r[s[top--]] = i;
l[i] = s[top];
s[++top] = i;
}
while (top) l[s[top--]] = 0;
LL ans = -INF;
for (int i = 1; i <= n; i++) ans = max(ans ,1ll * (r[i] - l[i] - 1) * a[i]);
printf("%lld\n" ,ans);
}
return 0;
}
Problem 4 题目描述:
给定一个 \(n \times m\) 的矩阵,求出这个矩阵中的最大全 0
矩阵的面积。
\(1 \leq n,m \leq 2000\) 。
Solution
可以分行考虑:假设我们在考虑前 \(i\) 行最大的全 \(0\) 面积。
可以把所有 \(0\) 的位置涂上颜色,然后把从 \(i\) 开始网上看一列涂上了颜色的格子看成一整个矩形(这里只考虑第从第 \(i\) 行涂了颜色的),设涂了颜色的矩形高度是 \(h_i\) ,那么原问题就变成了:
从 \(n\) 个长度分别为 \(h_1,h_2 \dots h_n\) ,宽度都为 \(1\) 的放在一起的矩形,求这些矩形的子矩形的面积的最大值。
这个问题可以使用单调栈来解决:如果我们要在 \([l,r]\) 这个范围内选出一个合法的最大的矩形,它的面积就是 \((r-l+1) \times \min_{i=l}^r h_i\) ,然后就可以用单调栈维护出每一个数左边第一个比它小的位置,右边第一个比它大的位置,就可以求了。
举个例子,对于下面的数据:
1 1 1 1
0 0 1 0
1 0 0 0
1 0 1 0
画出来是这个样子的:
如果我们分别对每一行求出它的 \(h\) 值,列成另一个矩阵是这样的:
0 0 0 0
1 1 0 1
0 2 1 2
0 3 0 3
对于每一行分别求解即可,代码如下:
#include <cstdio>
#include <cstring>
#include <cctype>
#include <algorithm>
#include <queue>
#include <iostream>
typedef long long LL;
using namespace std;
inline int read() {
int num = 0 ,f = 1; char c = getchar();
while (!isdigit(c)) f = c == '-' ? -1 : f ,c = getchar();
while (isdigit(c)) num = (num << 1) + (num << 3) + (c ^ 48) ,c = getchar();
return num * f;
}
const int N = 2005;
int a[N][N] ,s[N] ,ans ,top ,n ,m ,l[N] ,r[N];
inline int calc(int a[] ,int n) {
int ans = 0 ,top = 0;
for (int i = 1; i <= n; i++) {
while (top && a[s[top]] > a[i]) r[s[top--]] = i;
l[i] = s[top];
s[++top] = i;
}
for (int i = 1; i <= n; i++) ans = max(ans ,(r[i] - l[i] - 1) * a[i]);
return ans;
}
signed main() {
n = read();
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
int c = read();
if (c == 0) a[i][j] = a[i - 1][j] + 1;
}
for (int i = 1; i <= n; i++) ans = max(calc(a[i] ,n) ,ans);
printf("%d\n" ,ans);
return 0;
}