首页 > 其他分享 >单调栈学习笔记

单调栈学习笔记

时间:2023-04-24 20:23:45浏览次数:46  
标签:满足条件 元素 栈顶 笔记 学习 不等号 序列 单调

单调栈基础

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

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

我在选择什么栈,选择栈后弹栈不等号方向怎么写,每次都得绕一会才能想明白,给我自己做个笔记。

考虑你往严格递增的序列每两个相邻的数中间应该填什么不等号?对了,小于号。

严格递增栈的意思其实就是一个从 栈底栈顶 的严格递增序列,所以栈底对应序列的开头,栈顶对应序列的末尾。对于其它类型的栈也是如此,单调性指的都是栈底到栈顶这个方向。

换句话说,如果你的思维惯性地认为序列的开头在左侧,序列的结尾在右侧,那应该认为栈应该是向右开口的,在右面进出数,对应栈顶弹出和射入,序列尾部加数删数。

现在回来看:

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

可以翻译为:

严格递增序列。必须删除尾数,或尾数小于要新加的数后,才把新数加入到序列的尾部。

是不是就很明显了?

比较栈顶和要加入的数的过程,应该是比较到我们所维护的那个单调性满足为止才入栈。我们在比较的时候,推荐在不等号的左面放上尾数,不等号的右面放上想要新加的数。因为如果这个新加的数真的加进去了,尾数应该在新加的数的左面。这样以来,不等号的选取就可以根据:单调递增的序列应该在每两个相邻数之间填写什么符号?来直接移植,不用动脑。

举个例子吧。如果我们要写单调非严格递减栈,分三步:

  • 考虑单调非严格递减序列,每两个数之间应该填写大于等于号。
  • 所以,将尾数写在左面,新加的数写在右面,中间的符号也应该是大于等于号。
  • 这就是加栈的条件了:栈为空,或栈顶大于等于新加数。
  • 那么弹栈的条件就是它的取反条件,直接套个 ! 即可。
  • while(!(!st.empty() || st.top() >= a[i])) st.pop();
  • 当然熟练了就会觉得上面这个写法很蠢了,直接将这个条件写成取反形式,如下:
  • while(st.empty() && st.top() < a[i]) st.pop();
  • 这个小于号也可以理解成:小于号会破坏我们要求的大于等于。

总之,将栈顶放在左侧,新加的数放在右侧,之间的不等号就能直接从对应单调序列中相邻元素之间的不等号来直接判断,就再也不用绕了。

单调栈基本性质

一些基本性质:

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

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

NGE

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

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

这个问题被称为 Next Greater Element 问题,即 NGE 问题。

上面这个问题要求解的是下一个比 \(a_i\) 大的数,也就是最小的 \(j >i\) 满足 \(a_j > a_i\)。我们换个写法,\(a_i <a_j\),原因是我们期望把在序列左边的元素放在不等号的左边。然后,我们挑选与这个不等号 相反的 不等号,对应的单调栈。比如这里相反的不等号是 \(\ge\)(注意这里原来不取等的现在取等,原来取等的现在不取等),相邻两个元素之间是这个不等号的应该是非严格递减序列,所以我们应该选择非严格递减栈。

于是,从左到右扫,每个数都会被入栈一次。如果 \(a_i\) 是 \(a_j\) 进栈时被弹出的,那么 \(f(i) = j\)。如果 \(a_i\) 没被弹出过,\(f(i) = -1\)。

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

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

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

同时,考虑任意时刻非严格单调递减栈任意两个相邻的元素 \(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\)。

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

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

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

这个结论可以推出:如果 \(i <k <j\),有 \(a_i \ge a_k \ge a_j\),并且 \(a_i\) 和 \(a_j\) 都在栈里,则这两个元素在单调栈中间一定还有元素(不一定是 \(a_k\),但一定有元素)。

区间端点最大 / 小问题

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)\) 满足要求。考虑维护单调非严格递减栈。

在射入 \(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() && s.top().first < x) {
            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;
}

标签:满足条件,元素,栈顶,笔记,学习,不等号,序列,单调
From: https://www.cnblogs.com/crab-in-the-northeast/p/monotonous-stack.html

相关文章

  • 爬取青年大学习
    importrequestsfromlxmlimportetreeurl='http://news.cyol.com/gb/channels/vrGlAKDl/index.html'headers={'User-Agent':'Mozilla/5.0(WindowsNT10.0;Win64;x64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/110.0.0.0......
  • 【学习笔记】快速傅里叶变换
    怎么有人省选后才来学FFT啊由于时间原因,本篇笔记仅为个人总结,真正想要学习FFT的请参看这篇博客。前置知识单位根性质:$w_n^{2k}=w_{n/2}^k$$w_n^a+w_n^b=w_n^{a+b}$算法原理可知n+1个点可以唯一确定一条n次多项式,于是可以用n个点的点对集合表示一条曲线。......
  • BSGS(大步小步算法)学习笔记
    解决高次同余问题。\(a^x\equivb(\modp)\),其中\(a\)与\(p\)同余。这个形式与欧拉定理类似。思想:meetinthemiddle(折半搜索)。具体的,令\(x=A\timest-B\),且\(x\)一定在\([0,\phi(p))\)的范围内。但是\(p\)是质数时复杂度还是会爆炸。将\(x=A\timest-B\)带入......
  • Python学习笔记--json序列化时间报错-改源码
    问题:转换时间报错执行代码为:importjsonfromdatetimeimportdate,datetimed={"time1":date.today(),"time2":datetime.today()}res=json.dumps(d)#报错  TypeError:ObjectoftypedateisnotJSONserializable方案1:手动转换str()方案2:继承类......
  • 云原生周刊:2023 年 Java 开发人员可以学习的 25 大技术技能
    文章推荐2023年Java开发人员可以学习的25大技术技能这篇文章为Java开发人员提供了2023年需要学习的一些重要技能,这些技能涵盖了现代Java开发、大数据和人工智能、安全性、分布式系统和区块链、以及其他领域。Java开发人员应该根据自己的需求和职业规划,选择适合自己......
  • Vue笔记汇总
    Vue3快速上手1.Vue3简介2020年9月18日,Vue.js发布3.0版本,代号:OnePiece(海贼王)耗时2年多、2600+次提交、30+个RFC、600+次PR、99位贡献者github上的tags地址:https://github.com/vuejs/vue-next/releases/tag/v3.0.02.Vue3带来了什么1.性能的提升打包大小减少41%初次......
  • GPU服务研究学习...
    windows10版本安装CUDA,首先需要下载两个安装包CUDAtoolkit(toolkit就是指工具包)cuDNN #安装CUDA教程https://developer.nvidia.com/cuda-downloads #安装cuDNN教程https://developer.nvidia.com/cudnn 安装完毕后验证#查看Cuda版本nvcc--version #......
  • XML Schema学习
    XMLSchema简介XMLSchena的作用是定义XML文档的划分构建模块。XMLSchema是基于XML的DTD代替者,XMLSchema可描述XML文档的结构。定义可出现在文档中的元素定义可出现在文档中的属性定义那个元素是子元素定义子元素的次序定义子元素的数目定义元素是否为空,或者是否包含文......
  • TypeScript 学习笔记 — 数组常见的类型转换操作记录(十四)
    获取长度lengthtypeLengthOfTuple<Textendsany[]>=T["length"];typeA=LengthOfTuple<["B","F","E"]>;//3typeB=LengthOfTuple<[]>;//0取第一项FirstItemtypeFirstItem<Textendsany[]>......
  • 【WPF学习】05 数据绑定
    如何实现WPF窗口内元素控件之间的数据绑定传统方式——先在XAML界面为对应控件设置建立相互关联所需要的属性,再在窗口后台编写业务代码这里以一个滑动条slider和三个文本框textbox之间的数据绑定为例:按照传统方式: 后台业务代码: 但在WPF里我们无需编写这种数据转换和传值......