单调栈学习笔记

先通过一道题目来引入这个数据结构

例题 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]\) ,使得:

\[(\sum_{i=l}^r a_i) \times(\min_{i=l}^r a_i) \]

最大,你需要输出这个最大值,以及你选出的区间的左、右端点。

\(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

画出来是这个样子的:

image-20210417173140487

如果我们分别对每一行求出它的 \(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;
}
posted @ 2021-04-18 13:54  recollector  阅读(45)  评论(0编辑  收藏  举报