单调栈与单调队列
单调栈
单调栈(Monotonic Stack)是一种特殊的栈结构,用于解决一些与单调性相关的问题。单调栈可以分为单调递增栈和单调递减栈两种类型。
单调递增栈:栈中元素保持单调递增的顺序,即栈底到栈顶元素依次增大。
单调递减栈:栈中元素保持单调递减的顺序,即栈底到栈顶元素依次减小。
单调栈常用于解决求解某个元素向左或向右第一个比它大或小的元素的位置或值的问题。在遍历数组或序列的过程中,利用单调栈可以有效地找到符合条件的元素。
例题:单调栈
题目描述
给定一个长度为
输入格式
第一行 输入一个数字
第二行 输入
数据范围:
输出格式
共一行,包含
输入样例
6
3 7 5 4 2 9
输出样例
-1 3 3 3 -1 2
题目分析
首先考虑暴力做法,外层循环
假设当前枚举到 a[j] > a[k] && j < k
则数 a[j]
用于不会被作为答案输出, 对于
示例代码
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int stk[N], tt;
int main() {
int n;
cin >> n;
for (int i = 0; i < n; i++) cin >> a[i];
for (int i = 0; i < n; i++) {
while (stk[tt] >= a[i]) tt--; // 若栈顶元素>=a[i],出栈
if (tt == 0) cout << "-1 "; // 栈为空,表示没有比我小的数
else cout << stk[tt] << " "; // 否则栈顶元素就是满足的结果
stk[++tt] = a[i]; // 当前元素入栈
}
return 0;
}
单调队列
单调队列(Monotonic Queue)是一种特殊的队列结构,它具有保持队列元素单调性(单调递增或单调递减)的特点。单调队列通常用于解决一些与滑动窗口相关的问题,例如找出滑动窗口中的最大值或最小值。
单调队列的特性在于,队列中的元素按照一定的顺序排列,使得在滑动窗口移动的过程中,能够高效地维护当前窗口内的最值,并且能够在O(1)时间内获取到最值。
具体实现单调队列的方法有多种,常见的是利用双端队列(Deque)结构来实现。在向队列中添加元素时,保持队列的单调性,即在保持单调递增(或单调递减)的情况下,添加元素;而在队列中存在破坏单调性的元素时,则将其移除。
单调队列的应用场景包括 滑动窗口最大值、滑动窗口最小值、滑动窗口中位数 等问题,在这些问题中,单调队列能够提供高效的解决方案。
例题:滑动窗口
题目描述
给定一个长度为
下面是一个例子:
该数组为 [1 3 -1 -3 5 3 6 7], k = 3。
窗口位置 | 最小值 | 最大值 |
---|---|---|
[1 3 -1] -3 5 3 6 7 | -1 | 3 |
1 [3 -1 -3] 5 3 6 7 | -3 | 3 |
1 3 [-1 -3 5] 3 6 7 | -3 | 5 |
1 3 -1 [-3 5 3] 6 7 | -3 | 5 |
1 3 -1 -3 [5 3 6] 7 | 3 | 6 |
1 3 -1 -3 5 [3 6 7] | 3 | 7 |
输入格式
输入包括两行。
第一行包括
第二行包括
数据范围:
输出格式
输出包括两行。
第一行包括窗口从左至右移动的每个位置的最小值。
第二行包括窗口从左至右移动的每个位置的最大值。
输入样例
8 3
1 3 -1 -3 5 3 6 7
输出样例
-1 -3 -3 -3 3 3
3 3 5 5 6 7
题目分析
以找滑动窗口内的最大值为例,我们需要解决的就两个问题:
- 维护一个大小为
的滑动窗口。 - 找出当前窗口里的最大值。
首先第一个问题很容易实现,只要在这个元素入队前,计算下当前队列里的元素数量,如果到了
而第二个问题,容易想到直接遍历一遍队列找最大值,但这样操作时间复杂度为
试想一下,按当前方式维护的滑动窗口,里面是有大量无用的数据的。什么叫无用的数据呢?对于该题,就是不可能成为答案的数据,这里要实现的功能是找最大值,那么当当前元素入队的时候,前面所有比它小的元素必然不可能再作为答案了,因为当前元素不管是从大小、还是从“存活”时间来说,都是比前面这些元素要强的。
也就意味着,每次新元素入队前,可以把前面所有比它小的元素出队,经过这样操作,会发现,队列里面始终保持着单调递减的性质,所以每个滑动窗口的最大值就是当前的队首元素。这便是单调队列的妙用。
至于时间复杂度,队列只会把每个元素访问一遍,所以复杂度为
那么同理,滑动窗口最小值也可以依照上面的方式来实现。
示例代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int a[N], q[N], hh, tt = -1; // q队列为双端队列,且存的是下标
int main() {
int n, k;
scanf("%d%d", &n, &k);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
for (int i = 0; i < n; i++) {
while (hh <= tt && i - k + 1 > q[hh]) hh++; // 维护窗口大小
while (hh <= tt && a[q[tt]] >= a[i]) tt--; // 入队且维护单调性
q[++tt] = i;
if (i - k + 1 >= 0) printf("%d ", a[q[hh]]); // 判断符合窗口大小才输出
}
printf("\n");
hh = 0, tt = -1; // 重新初始化队头队尾
for (int i = 0; i < n; i++) {
while (hh <= tt && i - k + 1 > q[hh]) hh++;
while (hh <= tt && a[q[tt]] <= a[i]) tt--;
q[++tt] = i;
if (i - k + 1 >= 0) printf("%d ", a[q[hh]]);
}
return 0;
}