单调栈学习笔记

单调栈基础

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

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

单调栈基本性质

一些基本性质:

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

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

NGE

NGE 基础

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

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

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

于是,从左到右扫,每个数都会被入栈一次。如果 aia_iaja_j 进栈时被弹出的,说明 ai<aja_i < a_j,那么 f(i)=jf(i) = j。如果 aia_i 没被弹出过,f(i)=1f(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 单调栈为例。

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

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

同时,考虑任意时刻 NGE 单调栈任意两个相邻的元素 aia_iaja_j。设 aia_i 靠近栈底,aja_j 靠近栈顶,明显有 i<ji <jaiaja_i \ge a_j

如果 i<j1i < j - 1,则 aj1a_{j - 1} 不在栈里。由于 aj1a_{j - 1} 上一秒刚进栈,下一秒就被 aja_j 弹出了,所以 aj1<aja_{j - 1} < a_j

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

非常类似地,我们可以推出对于任意 i<k<ji < k <jak<aja_k < a_j

所以可以推出 aiaj>aka_i \ge a_j > a_k

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

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

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

区间端点最大 / 小问题

P1823 COI2007 Patrik 音乐会的等待

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

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

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

那么满足条件的 aia_i 又有哪些呢?

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

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

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

那么比 apa_p 还靠近栈底的元素是否满足条件?

分类讨论。

【第一种情况: ap=aj\boldsymbol{a_p = a_j}

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

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

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

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

【第二种情况: ap>aj\boldsymbol{a_p > a_j}

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

而不在栈顶的其它元素在序列上到 aja_j 一定会经过 ap>aja_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)(i, j) 数量,满足 ai>max(a[i+1j])a_i > \max(a[i + 1\ldots j])

【思路一】

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

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

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

弹出所有应该弹的元素后,栈内剩下的任一元素 apa_p 仍未找到它的 NNLE,也即,不存在一个 kk 使得 p<kjp < k \le j 满足 apaka_p \le a_k。换句话说,所有满足 p<kjp < k \le jkk 都满足 ap>aka_p > a_k,就有 ap>max(a[p+1j])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),对于一个 ii,所有满足条件的 jj 其实就是在 aia_iaia_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

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

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

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

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

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

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

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

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

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

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

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

统计完这个数量之后,直接用 nn 减去它即可。

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

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

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

只用考虑栈内的元素。

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

再考虑栈内剩余的任一元素 apa_p,有 ap<aja_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)(i, j) 的过程一般都是扫 jj 维护单调栈,看满足条件的 ii 有多少个。而且,不在单调栈里的 aia_i 对应的 ii 一定不合法。

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

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

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

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

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

满足对于任意 i<k<ji < k < j 都有 akaia_k \le a_i

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

满足存在 i<k<ji < k < j 使得 ak>aia_k > a_i

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

再来看这个例子:

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

首先砍去 aja_j 限制:

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

最值转任意 kk

对于任意 i<k<ji < k < jak<aia_k < a_i

破坏:

存在一个 i<k<ji < k < jakaia_k \ge a_i

所以用 NNLE 栈。

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

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

  • 为射入 aja_j,要弹出的元素。
  • 射入 aja_j 后仍为弹出的元素。

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

长方形

P1950 长方形

对于每个 ii1n1 \to n,统计底边在第 ii 行下边界的矩形数量。

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

然后就变成了 nn 次计数版的 HISTOGRA。问题转化成:

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

考虑一下怎么做。

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

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

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

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

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

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

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

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

很明显,hih_i 的 PLE 下标 <L<L,即 li<Ll_i < Lhih_i 的 NNGE >R>R,即 ri>Rr_i > R。这个 ii 满足条件。

对于任意 Lj<iL \le j < i,有 hjhih_j \ge h_i,这说明 hjh_j 的 NNGE 下标至少不会超过 ii,即 rjir_j \le i。因为 iRi \le R,所以 rjRr_j \le RRR 不落在 [j,rj1][j, r_j - 1] 内部,jj 不合法。

对于任意 i<kRi < k \le R,有 hi<hkh_i <h_k,这说明 hkh_k 的 LPE 下标至少不会小于 ii,即 lkil_k \ge i。因为 iLi \ge L,所以 lkLl_k \ge LLL 不落在 [lk+1,k][l_k + 1, k] 内部,kk 不合法。证毕。

也就是说,对于任何一个左端点为 LL,右端点为 RR 的矩形,只会在扫到 hh[L,R][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 @   dbxxx  阅读(177)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示