目录
单调栈和单调队列常用的类型比较少且相似度都非常大,首先我们先用暴力的思想来写一个时间复杂度较高的解决方法,而后再着手与优化,利用栈和队列的思想进行求解,降低时间复杂度
单调栈
理论依据
STL库函数#include <stack>或者手动模拟的数组,即
//初始化手动模拟栈
const int N = 1000000 + 10;
int stk[N],tt = 0;
//判断栈非空
if(hh){}
//元素入栈
stk[++ tt] = k;
//元素出栈
tt -- ;
常用的题型
求取某一个数组当中,每个数的左边满足某个条件最近的数,如果有则输出那个数,如果没有则输出-1表示无解,当数据量较小的时候我们可以想到利用二分查找来做,但是当数据量较大的时候二分查找往往就不能满足此种情况的时间复杂度了
单调栈
给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。
输入格式
第一行包含整数 N,表示数列长度。
第二行包含 N 个整数,表示整数数列。
输出格式
共一行,包含 N 个整数,其中第 i 个数表示第 i 个数的左边第一个比它小的数,如果不存在则输出 −1。
数据范围
1≤N≤10^5
1≤数列中元素≤10^9
输入样例:
5
3 4 2 7 5
输出样例:
-1 3 -1 2 2
解题思路
首先我们对于暴力的朴素做法进行模拟,暴力的做法往往是利用两重for循环,外层for循环遍历数组,内层for循环从外层的循环元素的前一位开始倒查,若没查到则输出-1,查到了输出满足条件的那个数break即可。时间复杂度是非常高的,那么在这些数组元素当中,是否存在某些元素一定不会被输出呢?我们可以利用一个栈来对于数组当中某个元素之前的所有元素进行存储,从栈当中搜索答案即可,那么对于栈当中元素的推入与删除也有着条件,本着栈先入后出、删除尾部的原则,从头到尾开始遍历数组,在遍历的同时开始对于满足条件的元素进行推入,不满足条件的元素进行删除,即当遍历至某个元素时,若遍历到的此元素小于等于栈顶元素,就开始对于栈顶元素进行删除,直至满足遍历至的元素大于栈顶元素或者栈被清空时停止,那么所要求的此元素的满足条件的元素即为栈顶元素,若栈被删空,即代表着该栈中没有满足小于此元素的元素,即无解输出-1。利用模拟栈,我们即可实现遍历一遍数组从而求取答案的解法
只要在栈中存在着ax >= ay且x < y的情况,那么ax就可以进行删去,那么栈中存在的序列的话也就一定是一的单调递增的序列了,这也就是单调栈的由来
假设我们遍历值下标为k的元素,此时单调栈当中的元素状况就如图所示:
蓝色叉为删除的栈中的逆序对,剩余元素由彩虹线连接而成显示出单调递增的趋势,那么满足k条件的元素即为栈顶元素也就是箭头所指之处
源代码
#include <iostream>
using namespace std;
const int N = 1000000 + 10;
int stk[N],tt = 0;
int main()
{
int n;
cin >> n;
while(n -- )
{
int x;
cin >> x;
while(tt && x <= stk[tt])tt -- ;
if(tt)cout << stk[tt] << ' ';
else cout << "-1" << ' ';
stk[++ tt] = x;
}
return 0;
}
单调队列
理论依据
因为一般滑动窗口不是简单的队列,所以可以用优先队列和双端队列来做
可以STL库也可以数组模拟
//对于模拟双端队列进行初始化
const int N = 1000000 + 10;
int q[N],hh = 0,tt = -1;
//判断双端队列非空
if(hh <= tt)
//推入队列元素
q[++ tt] = x;
//出队操作
tt -- ;
常用的题型
常用于求取某一个数组当中,一个滑动窗口当中的最大值或者最小值,在窗口不断往后移动的过程之中,不断输出每次窗口的满足条件的值,从暴力入手,而后优化
滑动窗口
给定一个大小为 n≤10^6 的数组。
有一个大小为 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
解题思路
若是暴力做法的话,则利用窗口一步一步后移,在每次后移的同时,遍历窗口的长度从而查找满足条件的值,注意数据范围,若是如此暴力的话数据量是极其恐怖的,TLE是常态。双端队列---“边吃边拉”。双端队列中所存储的不是值,而是元素的下标,也因此我们可以对于窗口移动的处理进行简化。窗口左端先移动,窗口右端后缩小,左端点i,窗口长度为k,因此左端点的下标也就为i - k + 1,若是i - k + 1 > q[hh]且队列非空的话,即为右端要缩小了,hh ++ 即可。当即将新存入的元素小于等于队列当中的元素且队列非空时,进行出队操作,而后向右扩一位。队头即为答案,输出的时候当i >= k - 1时,才能够输出(i从0开始)。因为刚开始队列没有满的时候要先把元素压入队列当中。
是不是看完之后似懂非懂一头雾水,那么我带领你手动模拟一遍就懂了
声明:双端队列存储的皆为下标,为了方便观察,以下队列中的内容存储的为下标对应的元素,实际代码运行情况为队列中元素的对应下标,不要搞混
i = 0的时候,队列为空,a[q[hh]] = a[0] = 1
i = 1的时候,3大于1满足单调递增的情况,不删,入队 ,a[q[hh]] = a[0] = 1
i = 2的时候,此时窗口已经成型,-1在入队之前发现1和3均不满足单调递增,就以依次删除,队列之中仅留-1 ,此时输出队头元素,a[q[hh]] = a[2] = -1
i = 3的时候,不满足单调递增,-1出队,-3入队,a[q[hh]] = a[3] = -3
i = 4的时候,5入队, a[q[hh]] = a[3] = -3
i = 5的时候,3大于5不满足单调递增,5出队3入队,a[q[hh]] = a[3] = -3
i = 6的时候,-3超出了队列范围要出队,6入队,a[q[hh]] = a[5] =3
i = 7的时候,7入队,a[q[hh]] = a[3] = -3,a[q[hh]] = a[5] =3
因此答案输出为-1 -3 -3 -3 3 3
从手动模拟我们可以看出,双端队列实际就是将队列中调整为单调递减或者单调递增,队头用来作为答案输出,因此,对于滑动窗口之中最大的元素,我们只需要将单调队列调整为单调递减即可,开头必为滑动窗口之中的最大元素
源代码
#include <iostream>
using namespace std;
const int N = 1000000 + 10;
int q[N],a[N],hh,tt;
int main()
{
int n,k;
cin >> n >> k;
for(int i = 0;i < n;i ++ )cin >> a[i];
hh = 0,tt = -1;
for(int i = 0;i < n;i ++ )
{
if(hh <= tt && i - k + 1 > q[hh])hh ++ ;
while(hh <= tt && a[i] <= a[q[tt]])tt -- ;
q[++ tt] = i;
if(i >= k - 1)cout << a[q[hh]] << ' ';
}
cout << endl;
hh = 0,tt = -1;
for(int i = 0;i < n;i ++ )
{
if(hh <= tt && i - k + 1 > q[hh])hh ++ ;
while(hh <= tt && a[i] >= a[q[tt]])tt -- ;
q[++ tt] = i;
if(i >= k - 1)cout << a[q[hh]] << ' ';
}
}