单调栈

引入

何为单调栈?顾名思义,单调栈即满足单调性的栈结构。与单调队列相比,其只在一端进行进出。

为了描述方便,以下举例及伪代码以维护一个整数的单调递增栈为例。

过程

插入

将一个元素插入单调栈时,为了维护栈的单调性,需要在保证将该元素插入到栈顶后整个栈满足单调性的前提下弹出最少的元素。

例如,栈中自顶向下的元素为 \(\{0,11,45,81\}\)。

插入元素 \(14\) 时为了保证单调性需要依次弹出元素 \(0,11\),操作后栈变为 \(\{14,45,81\}\)。

用伪代码描述如下:

insert x
while !sta.empty() && sta.top()<x
sta.pop()
sta.push(x)

使用

自然就是从栈顶读出来一个元素,该元素满足单调性的某一端。

例如举例中取出的即栈中的最小值。

应用

\(\text{POJ3250 Bad Hair Day}\)

有 \(N\) 头牛从左到右排成一排,每头牛有一个高度 \(h_i\),设左数第 \(i\) 头牛与「它右边第一头高度 \(≥h_i\)」的牛之间有 \(c_i\) 头牛,试求 \(\sum_{i=1}^{N} c_i\)。

比较基础的应用有这一题,就是个单调栈的简单应用,记录每头牛被弹出的位置,如果没有被弹出过则为最远端,稍微处理一下即可计算出题目所需结果。

另外,单调栈也可以用于离线解决 \(\text{RMQ}\) 问题。

我们可以把所有询问按右端点排序,然后每次在序列上从左往右扫描到当前询问的右端点处,并把扫描到的元素插入到单调栈中。这样,每次回答询问时,单调栈中存储的值都是位置 \(\le r\) 的、可能成为答案的决策点,并且这些元素满足单调性质。此时,单调栈上第一个位置 \(\ge l\) 的元素就是当前询问的答案,这个过程可以用二分查找实现。使用单调栈解决 \(\text{RMQ}\) 问题的时间复杂度为 \(O(q\log q + q\log n)\),空间复杂度为 \(O(n)\)。

习题

单调队列

引入

在学习单调队列前,让我们先来看一道例题。

\(\text{Sliding Window}\)

本题大意是给出一个长度为 \(n\) 的数组,编程输出每 \(k\) 个连续的数中的最大值和最小值。

最暴力的想法很简单,对于每一段 \(i \sim i+k-1\) 的序列,逐个比较来找出最大值(和最小值),时间复杂度约为 \(O(n \times k)\)。

很显然,这其中进行了大量重复工作,除了开头 \(k-1\) 个和结尾 \(k-1\) 个数之外,每个数都进行了 \(k\) 次比较,而题中 \(100\%\) 的数据为 \(n \le 1000000\),当 \(k\) 稍大的情况下,显然会 \(\text{TLE}\)。

这时所用到的就是单调队列了。

定义

顾名思义,单调队列的重点分为「单调」和「队列」。

「单调」指的是元素的「规律」——递增(或递减)。

「队列」指的是元素只能从队头和队尾进行操作。

Ps. 单调队列中的 "队列" 与正常的队列有一定的区别,稍后会提到

例题分析

解释

有了上面「单调队列」的概念,很容易想到用单调队列进行优化。

要求的是每连续的 \(k\) 个数中的最大(最小)值,很明显,当一个数进入所要 "寻找" 最大值的范围中时,若这个数比其前面(先进队)的数要大,显然,前面的数会比这个数先出队且不再可能是最大值。

也就是说——当满足以上条件时,可将前面的数 "弹出",再将该数真正 \(\text{push}\) 进队尾。

这就相当于维护了一个递减的队列,符合单调队列的定义,减少了重复的比较次数,不仅如此,由于维护出的队伍是查询范围内的且是递减的,队头必定是该查询区域内的最大值,因此输出时只需输出队头即可。

显而易见的是,在这样的算法中,每个数只要进队与出队各一次,因此时间复杂度被降到了 \(O(N)\)。

而由于查询区间长度是固定的,超出查询空间的值再大也不能输出,因此还需要 \(\text{site}\) 数组记录第 \(i\) 个队中的数在原数组中的位置,以弹出越界的队头。

过程

例如我们构造一个单调递增的队列会如下:

原序列为:

1 3 -1 -3 5 3 6 7

因为我们始终要维护队列保证其 递增 的特点,所以会有如下的事情发生:

操作 队列状态
\(1\) 入队 {1}
\(3\) 比 \(1\) 大,\(3\) 入队 {1 3}
\(-1\) 比队列中所有元素小,所以清空队列 \(-1\) 入队 {-1}
\(-3\) 比队列中所有元素小,所以清空队列 \(-3\) 入队 {-3}
\(5\) 比 \(-3\) 大,直接入队 {-3 5}
\(3\) 比 \(5\) 小,\(5\) 出队,\(3\) 入队 {-3 3}
\(-3\) 已经在窗体外,所以 \(-3\) 出队;\(6\) 比 \(3\) 大,\(6\) 入队 {3 6}
\(7\) 比 \(6\) 大,\(7\) 入队 {3 6 7}

例题参考代码:

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#define maxn 1000100
using namespace std;
int q[maxn], a[maxn];
int n, k; void getmin() { // 得到这个队列里的最小值,直接找到最后的就行了
int head = 0, tail = -1;
for (int i = 1; i < k; i++) {
while (head <= tail && a[q[tail]] >= a[i]) tail--;
q[++tail] = i;
}
for (int i = k; i <= n; i++) {
while (head <= tail && a[q[tail]] >= a[i]) tail--;
q[++tail] = i;
while (q[head] <= i - k) head++;
printf("%d ", a[q[head]]);
}
} void getmax() { // 和上面同理
int head = 0, tail = -1;
for (int i = 1; i < k; i++) {
while (head <= tail && a[q[tail]] <= a[i]) tail--;
q[++tail] = i;
}
for (int i = k; i <= n; i++) {
while (head <= tail && a[q[tail]] <= a[i]) tail--;
q[++tail] = i;
while (q[head] <= i - k) head++;
printf("%d ", a[q[head]]);
}
} int main() {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
getmin();
printf("\n");
getmax();
printf("\n");
return 0;
}

Ps. 此处的 "队列" 跟普通队列的一大不同就在于可以从队尾进行操作,STL 中有类似的数据结构 deque。

例题 \(2\) \(\text{Luogu P2698 Flowerpot S}\)

给出 \(N\) 滴水的坐标,\(y\) 表示水滴的高度,\(x\) 表示它下落到 \(x\) 轴的位置。每滴水以每秒 1 个单位长度的速度下落。你需要把花盆放在 \(x\) 轴上的某个位置,使得从被花盆接着的第 1 滴水开始,到被花盆接着的最后 1 滴水结束,之间的时间差至少为 \(D\)。

我们认为,只要水滴落到 \(x\) 轴上,与花盆的边沿对齐,就认为被接住。给出 \(N\) 滴水的坐标和 \(D\) 的大小,请算出最小的花盆的宽度 \(W\)。

\(1\leq N \leq 100000 , 1 \leq D \leq 1000000, 0 \leq x,y\leq 10^6\)

将所有水滴按照 \(x\) 坐标排序之后,题意可以转化为求一个 \(x\) 坐标差最小的区间使得这个区间内 \(y\) 坐标的最大值和最小值之差至少为 \(D\)。我们发现这道题和上一道例题有相似之处,就是都与一个区间内的最大值最小值有关,但是这道题区间的大小不确定,而且区间大小本身还是我们要求的答案。

我们依然可以使用一个递增,一个递减两个单调队列在 \(R\) 不断后移时维护 \([L,R]\) 内的最大值和最小值,不过此时我们发现,如果 \(L\) 固定,那么 \([L,R]\) 内的最大值只会越来越大,最小值只会越来越小,所以设 \(f(R) = \max[L,R]-\min[L,R]\),则 \(f(R)\) 是个关于 \(R\) 的递增函数,故 \(f(R)\geq D \implies f(r)\geq D,R\lt r \leq N\)。这说明对于每个固定的 \(L\),向右第一个满足条件的 \(R\) 就是最优答案。

所以我们整体求解的过程就是,先固定 \(L\),从前往后移动 \(R\),使用两个单调队列维护 \([L,R]\) 的最值。当找到了第一个满足条件的 \(R\),就更新答案并将 \(L\) 也向后移动。随着 \(L\) 向后移动,两个单调队列都需及时弹出队头。这样,直到 \(R\) 移到最后,每个元素依然是各进出队列一次,保证了 \(O(n)\) 的时间复杂度。

参考代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
typedef long long ll;
int mxq[N], mnq[N];
int D, ans, n, hx, rx, hn, rn; struct la {
int x, y; bool operator<(const la &y) const { return x < y.x; }
} a[N]; int main() {
scanf("%d%d", &n, &D);
for (int i = 1; i <= n; ++i) {
scanf("%d%d", &a[i].x, &a[i].y);
}
sort(a + 1, a + n + 1);
hx = hn = 1;
ans = 2e9;
int L = 1;
for (int i = 1; i <= n; ++i) {
while (hx <= rx && a[mxq[rx]].y < a[i].y) rx--;
mxq[++rx] = i;
while (hn <= rn && a[mnq[rn]].y > a[i].y) rn--;
mnq[++rn] = i;
while (L <= i && a[mxq[hx]].y - a[mnq[hn]].y >= D) {
ans = min(ans, a[i].x - a[L].x);
L++;
while (hx <= rx && mxq[hx] < L) hx++;
while (hn <= rn && mnq[hn] < L) hn++;
}
}
if (ans < 2e9)
printf("%d\n", ans);
else
puts("-1");
return 0;
}

Day 3 - 单调栈、单调队列、凸包与斜率优化的更多相关文章

  1. 单调栈&单调队列入门

    单调队列是什么呢?可以直接从问题开始来展开. Poj 2823 给定一个数列,从左至右输出每个长度为m的数列段内的最小数和最大数. 数列长度:\(N <=10^6 ,m<=N\) 解法① ...

  2. 单调栈&单调队列学习笔记!

    ummm,,,都是单调系列就都一起学了算了思想应该都差不多呢qwq 其实感觉这俩没有什么可说的鸭QAQ就是维护一个单调的东西,区别在于单调栈是一段进一段出然后单调队列是一段进另一段出?没了 好趴辣重点 ...

  3. 小结:单调栈 & 单调队列

    概要: 对于维护信息具有单调性的性质或者问题可以转化为具有单调性质的模型的题,我们可以考虑用单调栈或单调队列. 技巧及注意: 技巧很多,只要能将问题转化为单调性问题,就好解决了. 当维护固定长度的单调 ...

  4. 单调栈&单调队列

    最近打了三场比赛疯狂碰到单调栈和单调队列的题目,第一,二两场每场各一个单调栈,第三场就碰到单调队列了.于是乎就查各种博客,找单调栈,单调队列的模板题去做,搞着搞着发现其实这两个其实是一回事,只不过利用 ...

  5. [CSP-S模拟测试]:Cover(单调栈++单调队列+DP)

    题目传送门(内部题126) 输入格式 第一行两个个整数$n,m$表示区间的长度与彩灯的数量. 接下来$m$行,每行三个整数$l_i,r_i,a_i$表示一条彩灯能够覆盖的区间以及它的美观程度. 输出格 ...

  6. HZNU-ACM寒假集训Day10小结 单调栈-单调队列

    数据结构往往可以在不改变主算法的前提下题高运行效率,具体做法可能千差万别,但思路却是有规律可循 经典问题:滑动窗口  单调队列O(n) POJ 2823 我开始写的: TLE 说明STL的库还是有点慢 ...

  7. POJ 3250 Bad Hair Day --单调栈(单调队列?)

    维护一个单调栈,保持从大到小的顺序,每次加入一个元素都将其推到尽可能栈底,知道碰到一个比他大的,然后res+=tail,说明这个cow的头可以被前面tail个cow看到.如果中间出现一个超级高的,自然 ...

  8. UOJ#7 NOI2014 购票 点分治+凸包二分 斜率优化DP

    [NOI2014]购票 链接:http://uoj.ac/problem/7 因为太麻烦了,而且暴露了我很多学习不扎实的问题,所以记录一下具体做法. 主要算法:点分治+凸包优化斜率DP. 因为$q_i ...

  9. ACM数据结构-单调栈、队列

    1.最大数 代码: #include <stdio.h> #include <memory.h> #include <math.h> #include <st ...

  10. BZOJ 3203 Luogu P3299 [SDOI2013]保护出题人 (凸包、斜率优化、二分)

    惊了,我怎么这么菜啊.. 题目链接: (bzoj)https://www.lydsy.com/JudgeOnline/problem.php?id=3203 (luogu)https://www.lu ...

随机推荐

  1. post请求和get请求区别及其实例

    1.一般我们在浏览器输入一个网址访问网站都是GET请求;在FORM表单中,可以通过设置Method指定提交方式为GET或者POST提交方式,默认为GET提交方式.HTTP定义了与服务器交互的不同方法, ...

  2. 申请并部署免费的 SSL/TLS 证书

    对于囊中羞涩的我们来说,只要能白嫖,就绝不乱花钱.惯常申请免费 SSL/TLS 证书的途径有: 各大云服务平台限量提供.比如阿里云会给每个账号每年 20 个证书的申请额度.缺点是不支持泛域名,一年后须 ...

  3. GeoGebra作圆的切线

    参考文档:<GeoGebra入门教程>唐家军 1. 目的 使用GeoGebra作出过一点的圆的切线. 2. 构造过程 文档种的描述如下: 按照上述构造过程,在输入条形框中依次输入上面的指令 ...

  4. webpack处理静态资源

    像项目中字体资源是不需要进行打包处理的,可以直接的通过复制方式给打包到目标目录中 # 安装 npm i -D copy-webpack-plugin # 引入 const CopyPlugin = r ...

  5. kettle从入门到精通 第十三课 kettle 字符串操作

    1.本次示例讲解一些常用的字符串操作,有字段拼接,枚举值转换,计算器.字符串替换.字段拆分. 2.输入元数据有firstName.secondName.sex.salary.englishName.o ...

  6. CF914C

    problem & blog 数位 dp 模板题. 经过一次操作,可以把 \(n\) 变成一个小于 \(10^3\) 的数. 所以我们可以把所有小于 \(10^3\) 的数操作的次数全部处理出 ...

  7. MySQL插入中文数据时发生错误或者乱码的一些坑

    最近新入职的工作,火急火燎就下了个mysql,没想到安装时配置没弄好.今天在测试数据时,插入中文数据到mysql都是问号,先后查了半天修改表结构,数据库编码,my.ini文件都没有用. 首先第一步,打 ...

  8. 记一次 React context 使用

    学习 React 之 Context 使用 记录一次React context 使用 React.createContext Api 新建文件 contexts.js 文件用来存放 context 对 ...

  9. java并发和排序的简单例子(Runnable+TreeSet)

    很多时候并发需要考虑线程安全,但也有很多时候和线程安全毛关系都没有,因为并发最大的作用是并行,线程安全仅仅是并发的一个子话题. 例如常常会用于并发运算,并发i/o. 下文是一个练习笔记. 运行环境:w ...

  10. gitlab角色与权限

    用户在项目中的角色 Guest:访客.可以创建issue.发表评论,不能读写版本库.(就是看不了代码-) Reporter:Git项目测试人员.可以克隆代码,不能提交.QA.PM可以赋予这个权限. D ...