单调栈、单调队列
单调栈
单调递增栈:栈中数据入栈单调递增序列(栈底到栈顶是单调递增);
单调递减栈:栈中数据入栈单调递减序列(栈底到栈顶是单调递减)。
维护单调递增栈:遍历数组中每一个元素,执行入栈:每次入栈前先检验栈顶元素和进栈元素的大小。
如果栈空或进栈元素大于栈顶元素则直接入栈;如果进栈元素小于等于栈顶元素,则出栈,直至进栈元素大于栈顶元素。
//单调递增栈
int nums[maxn];
stack < int > q;
for (int i = 0; i < nums.length; i++) {
while (!q.empty() && nums[i] <= q.top() {
q.pop();
}
q.push(nums[i]);
}
维护单调递减栈:遍历数组中每一个元素,执行入栈:每次入栈前先检验栈顶元素和进栈元素的大小。
如果栈空或进栈元素小于栈顶元素则直接入栈;如果进栈元素大于等于栈顶元素,则出栈,直至进栈元素小于栈顶元素。
int nums[maxn];
stack < int > q;
for (int i = 0; i < nums.length; i++) {
while (!q.empty() && nums[i] >= q.top() {
q.pop();
}
q.push(nums[i]);
}
单调栈的作用
以O(N)的时间复杂度求出某个数的左边或右边第一个比它大或小的元素。
1、求第i个数左边第一个比它小的元素的位置(或者说是元素)
从左到右遍历元素构造单调递增栈:一个元素左边第一个比它小的数的位置就是将它插入单调递增栈时的栈顶元素,若栈为空,则说明不存在这样的数。
2、求第i个数左边第一个比它大的元素的位置
从左到右遍历元素构造单调递减栈:一个元素左边第一个比它大的数的位置就是将它插入单减栈时栈顶元素的值,若栈为空,则说明不存在这样的数。
3、求第i个数右边第一个比它小的元素的位置
从右到左遍历元素构造单调递增栈:一个元素右边第一个比它小的数的位置就是将它插入单增栈时栈顶元素的值,若栈为空,则说明不存在这样的数。
4、求第i个数右边第一个比它大的元素的位置
从右到左遍历元素构造单调递减栈:一个元素右边第一个比它大的数的位置就是将它插入单减栈时栈顶元素的值,若栈为空,则说明不存在这样的数。
典例
给定一个长度为N的整数序列,
1.输出每个数左边第一个比它小的数(或者求位置)—即是单调递增栈时的栈顶元素(位置则是下标)
2.输出每个数左边第一个比它大的数(或者求位置)—即是单减栈时栈顶元素的值(位置则是下标)
3、求第i个数右边第一个比它小的元素的数(或者求位置)—即是单调递增栈将它弹出栈时即将入栈的元素(位置则是下标)
4、求第i个数右边第一个比它大的元素的数(或者求位置)—即是单调递减栈将它弹出栈时即将入栈的元素的下标(位置则是下标)
如果不存在则输出-1.
典例一
给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。
输入格式
第一行包含整数 N,表示数列长度。
第二行包含 N 个整数,表示整数数列。
输出格式
共一行,包含 N 个整数,其中第 i 个数表示第 i 个数的左边第一个比它小的数,如果不存在则输出 −1。
数据范围
1≤N≤105
1≤数列中元素≤109
输入样例:
5
3 4 2 7 5
输出样例:
-1 3 -1 2 2
AC代码(求解每个数左边第一个比x大或者小的数)
#include <iostream>
using namespace std;
const int N = 100010;
int stk[N], tt;
int main()
{
int n;
cin >> n;
while (n -- )
{
int x;
scanf("%d", &x);
while (tt && stk[tt] >= x) tt -- ;//单调递增栈求左边第一个小于x的元素
while (tt && stk[tt] <= x) tt -- ;//单调递减栈求左边第一个大于x的元素
if (!tt) printf("-1 ");
else printf("%d ", stk[tt]);
stk[ ++ tt] = x;
}
return 0;
}
AC代码(求解x右边第一个比x大或者小的数)
#include <iostream>
#include <map>
using namespace std;
const int N = 100010;
int stk[N], tt;
int a[N],b[N];
int main()
{
int n;
cin >> n;
for(int i=1;i<=n;++i) scanf("%d", &a[i]);
for(int i=n;i>=1;--i){
int x=a[i];
while (tt && stk[tt] >= x) tt -- ;//递增求右边第一个比x小的数
while (tt && stk[tt] <= x) tt -- ;//递减求右边第一个比x大的数
if (!tt) b[i]=-1;
else b[i]=stk[tt];
stk[ ++ tt] = x;
}
for(int i=1;i<=n;++i) printf("%d ",b[i]);
return 0;
}
单调队列
作用:单调队列常用来维护给定区间中的最值问题,其时间复杂度为O(n)
1.单调队列必须满足从队头到队尾的严格单调性
2.单调递增队列:队列中的元素从小到大排列;保证队头元素是当前队列的最小值,用于维护区间最小值
单调递减队列:队列中的元素从大到小排列;保证队头元素是当前队列的最大值,用于维护区间最大值
3.单调队列需要完成的三个基本操作:
去尾:去掉尾部元素(目的:维护队列单调性,新元素入队时,去掉尾部不满足单调的尾部元素)
掐头:掐掉头部元素(条件:当前队头元素所在区间不在目标区间范围,目的:维护目标区间)
取最值:取出队列的头元素,即为最值
**eg:**对于单调递增队列,设当前准备入队的元素为e,从队尾开始把队列中的元素逐个与e对比,把比e大或者与e相等的元素逐个删除,直到遇到一个比e小的元素或者队列为空为止,然后把当前元素e插入到队尾。
对于单调递减队列也是同样道理,只不过从队尾删除的是比e小或者与e相等的元素
典例
滑动窗口
给定一个大小为 n≤106 的数组。
有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k 个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为 [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 |
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。 | ||
输入格式 | ||
输入包含两行。 | ||
第一行包含两个整数 n 和 k,分别代表数组长度和滑动窗口的长度。 | ||
第二行有 n个整数,代表数组的具体数值。 | ||
同行数据之间用空格隔开。 | ||
输出格式 | ||
输出包含两个。 | ||
第一行输出,从左至右,每个位置滑动窗口中的最小值。 | ||
第二行输出,从左至右,每个位置滑动窗口中的最大值。 | ||
输入样例: |
8 3
1 3 -1 -3 5 3 6 7
输出样例:
-1 -3 -3 -3 3 3
3 3 5 5 6 7
AC代码
#include <iostream>
using namespace std;
const int N = 1000010;
int a[N], q[N];
int main(){
int n, k;
scanf("%d%d", &n, &k);
for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ ){
if (hh <= tt && i - k + 1 > q[hh]) hh ++ ;
while (hh <= tt && a[q[tt]] >= a[i]) tt -- ;
q[ ++ tt] = i;
if (i >= k - 1) printf("%d ", a[q[hh]]);
}
puts("");
hh = 0, tt = -1;
for (int i = 0; i < n; i ++ ){
if (hh <= tt && i - k + 1 > q[hh]) hh ++ ;
while (hh <= tt && a[q[tt]] <= a[i]) tt -- ;
q[ ++ tt] = i;
if (i >= k - 1) printf("%d ", a[q[hh]]);
}
puts("");
return 0;
}