单调栈学习笔记

单调栈基础

单调栈根据所维护的单调性可以分为四种:

  • 严格递增栈。必须出栈至栈空或栈顶小于当前元素后,才入栈当前元素。
  • 严格递减栈。必须出栈至栈空或栈顶大于当前元素后,才入栈当前元素。
  • 非严格递增栈。必须出栈至栈空或栈顶小于等于当前元素后,才入栈当前元素。
  • 非严格递减栈。必须出栈至栈空或栈顶大于等于当前元素后,才入栈当前元素。

单调栈基本性质

一些基本性质:

  • 维护单调性的方式是:无论破坏多少个栈内的,也要让新元素进来。
  • 如果是正着扫序列,则越靠近栈顶的元素,下标越大,距离当前指针越近。

接下来来看单调栈的两个实例。

NGE

NGE 基础

当用到单调栈的时候,95% 都是在解决这个问题。

给定一个数组 \(a\),对每个 \(i\),求出 \(i\) 的下一个最近的比 \(a_i\) 大的数的下标 \(f(i)\),如果不存在则 \(-1\)

这个问题被称为 Next Greater Element 问题,即 NGE 问题。类似地,我们还有 NLE(Next Less Element),NNGE(Next Not Greater Element),NNLE(Next Not Less Element)问题。这四个问题分别对应四种不同的单调栈,对于 NGE 问题,我们应该选择非严格递减栈:必须出栈至栈空或栈顶大于等于当前元素后,才入栈当前元素。

于是,从左到右扫,每个数都会被入栈一次。如果 \(a_i\)\(a_j\) 进栈时被弹出的,说明 \(a_i < a_j\),那么 \(f(i) = j\)。如果 \(a_i\) 没被弹出过,\(f(i) = -1\)。至于这具体是为什么,证明省略,网上有很多。

/*
 * @Author: crab-in-the-northeast 
 * @Date: 2022-12-30 02:27:56 
 * @Last Modified by: crab-in-the-northeast
 * @Last Modified time: 2022-12-30 02:31:37
 */
#include <bits/stdc++.h>
inline int read() {
    int x = 0;
    bool f = true;
    char ch = getchar();
    for (; !isdigit(ch); ch = getchar())
        if (ch == '-')
            f = false;
    for (; isdigit(ch); ch = getchar())
        x = (x << 1) + (x << 3) + ch - '0';
    return f ? x : (~(x - 1));
}
typedef std :: pair <int, int> pii;

const int maxn = (int)3e6 + 5;
int ans[maxn];

int main() {
    int n = read();
    std :: stack <pii> s;

    for (int i = 1; i <= n; ++i) {
        int x = read();
        while (!s.empty() && x > s.top().first) {
            ans[s.top().second] = i;
            s.pop();
        }
        s.push({x, i});
    }

    for (int i = 1; i <= n; ++i)
        printf("%d ", ans[i]);
    puts("");
    return 0;
}

应该选择什么单调栈?

在实际做了一些单调栈题目后,我发现重要的不是你选择哪种单调性的单调栈,而是选择用于求解四个问题(NGE、NLE、NNGE、NNLE)中哪个问题的单调栈。

怎么选呢?假设我们要选择 NGE 单调栈,核心代码是这样的:

    for (int i = 1; i <= n; ++i) {
        int x = read();
        while (!s.empty() && x > s.top().first) {
            ans[s.top().second] = i;
            s.pop();
        }
        s.push({x, i});
    }

上面的 x > s.top().first 正是我们体现单调栈类型是 NGE 的地方。很好理解,s.top().first 就是栈顶的值,x 就是当前的元素,因为 x > s.top().first,所以栈顶找到了它的 NGE x,直接标记即可。

假设我们要选择 NNGE 单调栈,那么 while 里的条件应该写成:

!s.empty() && x <= s.top().first

因为 x 是栈顶的 Next Not Greater Element(即小于等于栈顶)。

当然,如果你实在想知道单调性和这四个问题的对应关系,这里我也给出:

  • NGE 单调栈 = 非严格单调递减栈。
  • NNLE 单调栈 = 严格单调递减栈。
  • NLE 单调栈 = 非严格单调递增栈。
  • NNGE 单调栈 = 严格单调递增栈。

你可以用适合自己的方法记忆。

从 NGE 问题发掘出更多性质

每个时刻单调栈内元素的含义是:待确定 NGE / NLE / NNGE / NNLE 的元素。这个思想尤为重要,可以看做单调栈的第三条性质。

下面我们以 NGE 单调栈为例。

在单调栈刚好射入 \(a_i\) 后,单调栈中的任一元素 \(a_j\),目前还没有找到它的 NGE。也即,\(a_j\) 的 NGE 要么不存在,要么下标 \(>i\),现在还没扫到呢。

而先前被插入,后来已不在单调栈内的元素 \(a_{j'}\),一定有 \(a_{j'}\) 的 NGE 存在,为 \(a_k\),并且下标 \(j' < k \le i\)

同时,考虑任意时刻 NGE 单调栈任意两个相邻的元素 \(a_i\)\(a_j\)。设 \(a_i\) 靠近栈底,\(a_j\) 靠近栈顶,明显有 \(i <j\)\(a_i \ge a_j\)

如果 \(i < j - 1\),则 \(a_{j - 1}\) 不在栈里。由于 \(a_{j - 1}\) 上一秒刚进栈,下一秒就被 \(a_j\) 弹出了,所以 \(a_{j - 1} < a_j\)

如果 \(i < j - 2\),意味着 \(a_{j - 2}\) 也没在栈里,那它要么被 \(a_{j - 1}\) 弹出去,要么被 \(a_{j}\) 弹出去。所以有 \(a_{j - 2} < a_{j - 1}\)\(a_{j - 2} < a_j\)。无论如何,因为 \(a_{j - 1} < a_j\),都有 \(a_{j - 2} < a_j\)

非常类似地,我们可以推出对于任意 \(i < k <j\)\(a_k < a_j\)

所以可以推出 \(a_i \ge a_j > a_k\)

这意味着,非严格单调递减栈中相邻的两个元素,在原序列中间夹着的所有元素,都比这两个元素更小。称之为性质四。

另外,对于 NNLE 栈,上面那个结论是 \(a_i > a_j \ge a_k\),证明思路类似。

NLE 和 NNGE 就是上面两种情况不等号反过来啦(这里不取等的接着不取等,取等的接着取等哦)。

区间端点最大 / 小问题

P1823 COI2007 Patrik 音乐会的等待

题意:统计数对 \((i, j)\) 的数量,满足 \(\max(a[i + 1 \ldots j - 1]) \le \min(a_i, a_j)\)

\(j\)\(1 \to n\),统计有多少个 \(i\) 满足 \((i, j)\) 满足要求。考虑维护 NGE 栈。

在射入 \(a_j\) 之前,不在单调栈内的元素 \(a_i\),要么还没进过栈(\(i \ge j\)),要么已经找到了 NGE(即存在一个 \(k\) 满足 \(i < k< j\)\(a_k > a_i\))。很明显,这两种情况对应的 \((i, j)\) 都是不满足要求的,所以我们只需要在单调栈中找满足条件的 \(a_i\) 即可。

那么满足条件的 \(a_i\) 又有哪些呢?

我们尝试将 \(a_j\) 射入,这样弹出若干次栈顶。设其中任一元素为 \(a_k\),则 \(a_k\) 的 NGE 为 \(a_j\)。也即,\(a_k\) 下一个比它大的数就是 \(a_j\),所以 \(\max(a[k + 1\ldots j - 1]) \le a_k < a_j\),很明显,\((k, j)\) 是满足要求的。

一直弹到不弹为止,很明显,如果此时栈已经为空,那么上面的所有 \((k, j)\) 已经是所有右端点为 \(j\) 的满足条件的数对了(因为不在栈里的一定不是)。

如果栈不为空,设栈顶为 \(a_p\)。则 \(a_p \ge a_j\)。此时如果我们射入 \(a_j\),那么 \(a_p\)\(a_j\) 就已经相邻了,之前推得的 性质四 说明,\((p, j)\) 也是满足条件的。

那么比 \(a_p\) 还靠近栈底的元素是否满足条件?

分类讨论。

【第一种情况:\(\boldsymbol{a_p = a_j}\)

考虑栈中等于 \(a_p\) 的其它元素 \(a_q\)。很明显所有满足条件的 \(a_q\) 都聚集在栈的顶部(我们先不急着射 \(a_j\))。

根据性质四,在原序列中,所有满足条件的 \(a_q\) 之间,\(a_q\)\(a_p\) 之间,\(a_p\)\(a_j\) 之间的元素都 \(< a_p = a_q = a_j\),所以所有的 \((q, j)\) 也是满足条件的。

考虑比 \(a_q\) 还大,又最靠近栈顶的元素 \(a_r\)。不难发现 \(a_r >a_q\),而且 \(a_r\) 再往栈顶走一个元素就是 \(a_q\)。考虑 \(a_r\) 和这个 \(a_q\) 之间的元素,同样满足都小于 \(a_q\)。所以 \((r, j)\) 也满足条件。

但是,\(a_r\) 再往栈底走一个元素 \(a_s\),即使 \(a_r = a_s\),序列上 \(s\)\(j\) 也会经过 \(a_r > a_j\),因此 \((s, j)\) 已经不再合法。比 \(a_s\) 更靠近栈底的元素也会经过 \(a_r\),同理不合法。

【第二种情况:\(\boldsymbol{a_p > a_j}\)

此时,只有栈顶这个 \(a_p\) 是满足条件的:如果再射入 \(a_j\)\(a_p\)\(a_j\) 将相邻,中间的元素 \(< a_j < a_p\),满足条件。

而不在栈顶的其它元素在序列上到 \(a_j\) 一定会经过 \(a_p > a_j\),不合法,遗憾离场。

到这里本题正确性已经做完了,但事实上上面的 第一种情况,我们还要从栈顶一直往栈底扫,找极长的一个元素相同段,复杂度已经不对了。

怎么办呢?这里有一个小技巧,那就是把栈中相邻的两个相等的元素“打包”,记成一个 pair 元素,其中 pair 的第一项是元素本身,第二项是元素出现了多少次。这样我们只需要获取栈顶信息即可,复杂度正确。

/*
 * @Author: crab-in-the-northeast 
 * @Date: 2023-04-24 19:49:57 
 * @Last Modified by: crab-in-the-northeast
 * @Last Modified time: 2023-04-24 20:09:39
 */
#include <bits/stdc++.h>
#define int long long
inline int read() {
    int x = 0;
    bool f = true;
    char ch = getchar();
    for (; !isdigit(ch); ch = getchar())
        if (ch == '-')
            f = false;
    for (; isdigit(ch); ch = getchar())
        x = (x << 1) + (x << 3) + ch - '0';
    return f ? x : (~(x - 1));
}
typedef std :: pair <int, int> pii;

signed main() {
    int n = read();
    std :: stack <pii> s;

    int ans = 0;
    while (n--) {
        int x = read();
        while (!s.empty() && x > s.top().first) {
            ans += s.top().second;
            s.pop();
        }
        if (s.empty())
            s.push({x, 1});
        else if (s.top().first == x) {
            int cnt = s.top().second;
            ans += cnt;
            s.pop();
            ans += (s.empty() ? 0 : 1);
            s.push({x, cnt + 1});
        } else {
            ++ans;
            s.push({x, 1});
        }
    }
    printf("%lld\n", ans);
    return 0;
}

对于其它类型的端点最值问题,该怎么解决?请接着阅读后面的例题。

例题

USACO06NOV Bad Hair Day S

P2866 USACO06NOV Bad Hair Day S

统计数对 \((i, j)\) 数量,满足 \(a_i > \max(a[i + 1\ldots j])\)

【思路一】

考虑扫 \(j\)\(1 \to n\) 统计 \((i, j)\) 的数量,并维护 NNLE 栈。

假设现在将要处理 \(a_j\)(刚处理完 \(a_{j - 1}\) 入栈),那么对于任意 \(i <j\),不在栈中的元素 \(a_i\) 都找到了它的 NNLE,即存在一个 \(k\) 满足 \(i < k < j\) 并且 \(a_i \le a_k\)。这样的 \((i, j)\) 不可能满足要求,因此只需判断单调栈中的元素。

考虑射入 \(a_j\),这样会弹出若干次栈顶,设弹出的任一元素为 \(a_k\)。此时,\(a_k\) 找到了它的 NNLE \(a_j\),证明 \(a_k \le a_j\),很明显 \((k, j)\) 不满足条件。

弹出所有应该弹的元素后,栈内剩下的任一元素 \(a_p\) 仍未找到它的 NNLE,也即,不存在一个 \(k\) 使得 \(p < k \le j\) 满足 \(a_p \le a_k\)。换句话说,所有满足 \(p < k \le j\)\(k\) 都满足 \(a_p > a_k\),就有 \(a_p > \max(a[p + 1\ldots j])\)

因此只需要让每个元素入栈时,先把该弹的弹掉,然后累计剩下的栈的大小即可。

/*
 * @Author: crab-in-the-northeast 
 * @Date: 2023-04-25 08:19:16 
 * @Last Modified by: crab-in-the-northeast
 * @Last Modified time: 2023-04-25 08:21:27
 */
#include <bits/stdc++.h>
#define int long long
inline int read() {
    int x = 0;
    bool f = true;
    char ch = getchar();
    for (; !isdigit(ch); ch = getchar())
        if (ch == '-')
            f = false;
    for (; isdigit(ch); ch = getchar())
        x = (x << 1) + (x << 3) + ch - '0';
    return f ? x : (~(x - 1));
}

signed main() {
    int n = read(), ans = 0;
    std :: stack <int> s;
    while (n--) {
        int x = read();
        while (!s.empty() && x >= s.top())
            s.pop();
        ans += s.size();
        s.push(x);
    }
    printf("%lld\n", ans);
    return 0;
}

【思路二】

上面这个思路很巧妙,但事实上有一个无脑做法。

考虑满足条件的 \((i, j)\),对于一个 \(i\),所有满足条件的 \(j\) 其实就是在 \(a_i\)\(a_i\) 的 NNLE 的两个下标之内的所有下标。

所以直接算 NNLE 即可。

HISTOGRA

SP1805 HISTOGRA - Largest Rectangle in a Histogram

考虑最优矩形,下方一定紧贴地线,上方一定紧贴某个矩形的高。

我们考虑对于给定每条小矩形,计算目标矩形如果在这个矩形上,以这个矩形的高为高,最宽能延伸多少。

然后发现转化成了 PLE(Previous Less Element)和 NLE 问题,用两个 NLE 栈维护,一个正着扫一个倒着扫即可。

/*
 * @Author: crab-in-the-northeast 
 * @Date: 2023-04-25 08:46:59 
 * @Last Modified by: crab-in-the-northeast
 * @Last Modified time: 2023-04-25 09:02:00
 */
#include <bits/stdc++.h>
#define int long long
inline int read() {
    int x = 0;
    bool f = true;
    char ch = getchar();
    for (; !isdigit(ch); ch = getchar())
        if (ch == '-')
            f = false;
    for (; isdigit(ch); ch = getchar())
        x = (x << 1) + (x << 3) + ch - '0';
    return f ? x : (~(x - 1));
}
inline bool gmx(int &a, int b) {
    return b > a ? a = b, true : false;
}

const int maxn = (int)1e5 + 5;
int a[maxn], ple[maxn], nle[maxn];

signed main() {
    for (int n = read(); n; n = read()) {
        for (int i = 1; i <= n; ++i)
            a[i] = read();

        std :: stack <int> s;
        for (int i = 1; i <= n; ++i) {
            while (!s.empty() && a[i] < a[s.top()]) {
                nle[s.top()] = i;
                s.pop();
            }
            s.push(i);
        }

        while (!s.empty()) {
            nle[s.top()] = n + 1;
            s.pop();
        }

        for (int i = n; i; --i) {
            while (!s.empty() && a[i] < a[s.top()]) {
                ple[s.top()] = i;
                s.pop();
            }
            s.push(i);
        }

        while (!s.empty()) {
            ple[s.top()] = 0;
            s.pop();
        }

        int ans = 0;
        for (int i = 1; i <= n; ++i)
            gmx(ans, a[i] * (nle[i] - 1 - ple[i]));
        printf("%lld\n", ans);
    }
    return 0;
}

POI2008 PLA-POSTERING

首先发现答案和矩形宽度一点关系都没有。忽略!

不难发现一种可行的方案:对每个建筑分别张贴一张海报,这样海报数量为 \(n\)

我们必须让一张海报同时完全覆盖两个建筑,才能让需要的海报数量变小。

考虑这两个建筑的必要条件:

  • 这两个建筑等高。
  • 这两个建筑中间没有比这两个建筑低的建筑。

不难发现上面的条件已经充分,我们考虑优先给满足上面条件的两个建筑中间填上矩形。

这样以来矩形可能有重叠,比如按照上面的填涂规则,有如下事例:

上图中,橙色矩形是红色矩形和黄色矩形的公共部分。

这个问题也很好处理,我们缩减红色的范围,让它的下界只扩展到黄色的上界就可以了。

类似的重叠问题都可以这样解决,优先满足下面的,让上面的只扩展到下面的上界即可。

因此,问题变成统计数对 \((i, j)\) 的数量,满足 \(\max(a[i + 1\ldots j - 1]) > a_i = a_j\)。这是端点最值问题,应用单调栈解决。

统计完这个数量之后,直接用 \(n\) 减去它即可。

解释一下上面为什么是 \(>\) 而不是 \(\ge\)。如果 \((i, j)\) 中间没有比他们高的元素,\((j, k)\) 中间没有比他们高的元素,那么可以有一个矩形精确覆盖 \(a[i \ldots k]\),这个时候需要的海报数量应该 \(-2\)。而如果是 \(\ge\),我们会把 \((i, k)\) 也统计上,从而会让需要海报数量 \(-3\),这不是我们期望的;而改成 \(>\) 就能解决这个问题了。

这个问题怎么做?考虑维护 NNGE 栈,扫 \(j\) 来统计 \((i, j)\) 的数量。

在射入 \(a_j\) 之前,不在栈内的任一元素 \(a_i\) 已经找到了它的 NNGE(Next Not Greater Element),说明存在 \(k\) 使得 \(i < k < j\)\(a_i \ge a_k\),这样以来 \((i, j)\) 已经不合法。

只用考虑栈内的元素。

考虑弹出为了射入 \(a_j\) 应该弹出的元素 \(a_k\),有 \(a_k\) 的 NNGE 是 \(a_j\),也即 \(a_{k + 1}\)\(a_{j - 1}\) 这一段严格比 \(a_k\) 大。这个时候,如果 \(a_k > a_j\),则 \((k, j)\) 不合法;如果 \(a_k = a_j\),则 \((k, j)\) 合法。

再考虑栈内剩余的任一元素 \(a_p\),有 \(a_p < a_j\),不合法。

所以只需要维护严格递增栈的同时,弹出栈顶的时候看栈顶有没有和元素相同的即可。

/*
 * @Author: crab-in-the-northeast 
 * @Date: 2023-04-25 13:36:30 
 * @Last Modified by: crab-in-the-northeast
 * @Last Modified time: 2023-04-25 13:52:14
 */
#include <bits/stdc++.h>
inline int read() {
    int x = 0;
    bool f = true;
    char ch = getchar();
    for (; !isdigit(ch); ch = getchar())
        if (ch == '-')
            f = false;
    for (; isdigit(ch); ch = getchar())
        x = (x << 1) + (x << 3) + ch - '0';
    return f ? x : (~(x - 1));
}

int main() {
    int n = read(), ans = n;
    std :: stack <int> s;
    for (int i = 1; i <= n; ++i) {
        read(); int x = read();
        while (!s.empty() && x <= s.top()) {
            if (s.top() == x)
                --ans;
            s.pop();
        }
        s.push(x);
    }
    printf("%d\n", ans);
    return 0;
}

我们已经看了两道类似端点最值问题的习题了,大概总结一下方法⑧。

首先,我们要明确,这种问题找到合法 \((i, j)\) 的过程一般都是扫 \(j\) 维护单调栈,看满足条件的 \(i\) 有多少个。而且,不在单调栈里的 \(a_i\) 对应的 \(i\) 一定不合法。

那么怎么确定找那种类型的单调栈呢?看已经确定了 \(\boldsymbol{a_i}\) 的 NGE,NNGE,NLE,NNLE 的四项中的哪一项时,\(\boldsymbol{(i, j)}\) 一定不合法。举个例子吧:

统计数对 \((i, j)\) 的数量,满足 \(\max(a[i + 1 \ldots j - 1]) \le \min(a_i, a_j)\)

为方便判断,我们将上面的最值形式做一些转化。首先就是先把 关于 \(\boldsymbol{a_j}\) 的限制砍去,即

满足 \(\max(a[i + 1 \ldots j - 1]) \le a_i\)

然后 将最值转成“对于任意 \(\boldsymbol k\)”这种形式,也即

满足对于任意 \(i < k < j\) 都有 \(a_k \le a_i\)

考虑 什么时候会破坏这个条件,显然,就是

满足存在 \(i < k < j\) 使得 \(a_k > a_i\)

也就是 \(a_i\) 找到了它的 NGE,判断成功,我们应该维护 NGE 栈,这样以来,不在栈内的元素就意味着找到 NGE,找到 NGE 就意味着不合法。

再来看这个例子:

统计满足 \(\max(a[i + 1\ldots j - 1]) < a_i = a_j\)

首先砍去 \(a_j\) 限制:

满足 \(\max(a[i + 1\ldots j-1]) < a_i\)

最值转任意 \(k\)

对于任意 \(i < k < j\)\(a_k < a_i\)

破坏:

存在一个 \(i < k < j\)\(a_k \ge a_i\)

所以用 NNLE 栈。

另外,上面那个 Bad Hair Day S 的思路一,也可以看成端点最值问题(只是不再将 \(a_j\) 看成端点了),所以用上面的方法也能很方便判断出应该使用什么栈,可以自己试试。

判断完应该使用哪个栈之后,剩余的部分就是看栈里的什么元素满足条件,一般分为两种看:

  • 为射入 \(a_j\),要弹出的元素。
  • 射入 \(a_j\) 后仍为弹出的元素。

根据一些取等细节,上面两类可能分为更细的类,需要自己判断。

长方形

P1950 长方形

对于每个 \(i\)\(1 \to n\),统计底边在第 \(i\) 行下边界的矩形数量。

从每个点 \((i, j)\) 出发向上往上最多能延伸多少,这个是可以 \(\Theta(nm)\) 预处理的。

然后就变成了 \(n\) 次计数版的 HISTOGRA。问题转化成:

HISTOGRA,但是计算矩形的数量。

考虑一下怎么做。

对第 \(i\) 个矩形,仍然设高度 \(h_i\),其 PLE 的下标为 \(l_i\),NLE 的下标为 \(r_i\)。那么从这个点开始,延伸的区域最多是 \([l_i +1, r_i - 1]\),尝试直接统计 \(h_i \times (i - l_i) \times (r_i - i)\)

这样以来可能会有重复计数吗?

考虑并排放置的两个等高小矩形,即 \(h_i = h_{i +1}\),则横长落在 \([i, i +1]\) 这个区间内的所有小矩形都会被 \(h_i \times (i - l_i) \times (r_i - i)\)\(h_{i +1} \times (i + 1 - l_{i + 1}) \times (r_{i + 1} - i - 1)\) 分别统计一次,有重复计数。

怎么办呢?考虑更换 \(r_i\) 的定义:\(h_i\) 的 NNGE 为 \(r_i\)。分析发现,原来的 \(r_i > i +1\),现在的 \(r_i = i + 1\),所以 \(h_i \times (i - l_i) \times (r_i - i)\) 不再会对 \(i + 1\) 那一列里的矩形计数,解决了重复计数问题。

我们验证一下是否真的不重不漏了。

每次以 \(i\) 为中心扩展时,统计了所有以 \([l_i + 1, i]\) 内某个点为左端点,以 \([i, r_i - 1]\) 内某个点为右端点的所有矩形。我们尝试证明:对于任意 \(L \le R\),有且仅有一个 \(i\),满足 \(L \in [l_i + 1, i]\)\(R \in [i, r_i - 1]\)

首先很明显,满足条件的 \(i \in [L, R]\)

其次,考虑找到 \(h[L \ldots R]\) 中取得最小值的 \(h_i\),如果有多个最小值,取 \(i\) 最大的一个。于是有:对于任意 \(L \le j < i\),有 \(h_j \ge h_i\),对于任意 \(i < k \le R\),有 \(h_k > h_i\)(这里不取等号是因为 \(i\) 在所有最小值中下标最大)。

很明显,\(h_i\) 的 PLE 下标 \(<L\),即 \(l_i < L\)\(h_i\) 的 NNGE \(>R\),即 \(r_i > R\)。这个 \(i\) 满足条件。

对于任意 \(L \le j < i\),有 \(h_j \ge h_i\),这说明 \(h_j\) 的 NNGE 下标至少不会超过 \(i\),即 \(r_j \le i\)。因为 \(i \le R\),所以 \(r_j \le R\)\(R\) 不落在 \([j, r_j - 1]\) 内部,\(j\) 不合法。

对于任意 \(i < k \le R\),有 \(h_i <h_k\),这说明 \(h_k\) 的 LPE 下标至少不会小于 \(i\),即 \(l_k \ge i\)。因为 \(i \ge L\),所以 \(l_k \ge L\)\(L\) 不落在 \([l_k + 1, k]\) 内部,\(k\) 不合法。证毕。

也就是说,对于任何一个左端点为 \(L\),右端点为 \(R\) 的矩形,只会在扫到 \(h\)\([L, R]\) 内最靠右的最小值这个点的时候才会被统计到。验证成功。

/*
 * @Author: crab-in-the-northeast 
 * @Date: 2023-04-25 17:53:24 
 * @Last Modified by: crab-in-the-northeast
 * @Last Modified time: 2023-04-25 18:04:25
 */
#include <bits/stdc++.h>
#define int long long
inline int read() {
    int x = 0;
    bool f = true;
    char ch = getchar();
    for (; !isdigit(ch); ch = getchar())
        if (ch == '-')
            f = false;
    for (; isdigit(ch); ch = getchar())
        x = (x << 1) + (x << 3) + ch - '0';
    return f ? x : (~(x - 1));
}
inline char rech() {
    char ch = getchar();
    while (!isgraph(ch))
        ch = getchar();
    return ch;
}

const int maxm = 1005;

int h[maxm], ple[maxm], nnge[maxm];

signed main() {
    int ans = 0, n = read(), m = read();
    while (n--) {
        for (int i = 1; i <= m; ++i)
            h[i] = (rech() == '.') ? (h[i] + 1) : 0;
        std :: stack <int> s;
        for (int i = 1; i <= m; ++i) {
            while (!s.empty() && h[i] <= h[s.top()]) {
                nnge[s.top()] = i;
                s.pop();
            }
            s.push(i);
        }
        while (!s.empty()) {
            nnge[s.top()] = m + 1;
            s.pop();
        }
        for (int i = m; i; --i) {
            while (!s.empty() && h[i] < h[s.top()]) {
                ple[s.top()] = i;
                s.pop();
            }
            s.push(i);
        }
        while (!s.empty()) {
            ple[s.top()] = 0;
            s.pop();
        }
        
        for (int i = 1; i <= m; ++i)
            ans += h[i] * (i - ple[i]) * (nnge[i] - i);
    }
    printf("%lld\n", ans);
    return 0;
}
posted @ 2023-04-24 20:13  dbxxx  阅读(129)  评论(1编辑  收藏  举报