最近学习了用数组模拟单调栈和单调队列,相较于STL,手动模拟栈和队列可以使印象更深。
下面是栈和队列的简单模拟。
首先我们理解下栈,栈是后进先出的。
即可以类似一个有底的箱子,最底部的东西是最先放入的,那么最底部的东西就会在其上的所有全部都被拿走后,才能被拿出。
关于栈有这几个操作
如图,绿色部分即栈顶元素,橙色部分即栈底元素。
skt是栈数组,tt是栈顶,即最靠近出口的部分
int skt[N], tt;
栈的操作
1.插入元素,栈是向栈顶插入元素
skt[++tt] = x;
2.弹出元素,遵循后进先出的原则
tt--;
3.判断栈是否为空
if(tt > 0) not empty
else empty
4.取栈顶元素
skt[tt];
-----------------------------------------------------------------------------------------------------------------------------------------------------------
接下来是队列的操作,首先我们需要理解下概念,与栈有些许区别,更适合从一个横向的人的排队思路去理解
如图红色先入队列,则其先出队列。
队列两边都是通的,所定义的是先进先出,如同一个人先去比赛,则他先结束比赛。
下面是队列的操作
q是队列数组,tt是队尾,hh是队首,初始化tt为-1,hh为0,则此时队列为空
int q[N], hh, tt = -1;
队列的操作
1.插入元素(后进后出,从队尾插入
q[++tt] = x;
2.弹出元素(先进先出,从队首弹出
hh++;
3.判断队列是否为空
if(hh < tt) not empty
else empty
4.取出队首元素
q[hh];
5.由于队列可以得到首尾的值,故队尾元素也可以取到
q[tt];
1.栈的应用——单调栈的应用 Acwing 830
传送门:https://www.acwing.com/problem/content/description/832/
给定一个长度为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
本题很显然如果通过暴力的话可能会TLE。
所以我们观察这个规律,从x左边第一个数开始判断其是否小于x,不小于则将其弹出栈,1.直到找到一个小于x的数,输出x或者2.tt已经为0,即栈已空,说明这里没有符合条件的数,则输出-1。
那么由个小疑问,比如若你弹出的数字符合条件小于x之后的数字,而这个数又比x小该怎么办
如4 3 5则依次输出-1 -1 3,这时你会发现,5前面总会有3离5更近而比5小,所以你弹出的数字一定不会符合之后数字的条件
所以就可以有
while( tt && q[tt] >= x) tt--;
if(tt) cout << q[tt] << ' ';
else cout << -1 << ' ';
2.队列的应用—— 滑动窗口 Acwing 154(单调队列)
传送门:https://www.acwing.com/problem/content/156/
给定一个大小为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
所以我们观察规律,发现了最小值和最大值可以通过单调性来处理,把我们的窗口处理为单调减和单调增的窗口,这样每次只要输出窗口的首元素即可。
但是这里可能有一个小疑惑,比如你删的元素若是下一个窗口的最大元素该如何
但实际上,这是不可能的。
比如有2 1 3,窗口为2,求每个窗口最小值
第一个窗口,因为1比2小,删除了2,而第二个窗口严格递增,故不需删除
再如 1 2 4 3 5,窗口为3,求每个窗口最小值
第一个窗口,1 2 4,已严格递增
第二个窗口,2 4 3, 删除4即保持严格递增
第三个窗口在第二次处理后只剩了 3 5 但是本来模样为 4 3 5,仍需要删除4,所以对其并无影响。
下面是标程:
#include<iostream>
using namespace std;
const int N = 1000010;
int q[N], a[N];
int main()
{
int n, k;
cin >> n >> k;
for(int i = 0; i < n; i++) cin >> 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) cout << a[q[hh]] << ' ';
//这里是保证我们的第一个窗口是q[0~k]的
}
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[q[tt]] <= a[i]) tt--; //保证窗口元素严格单调递增!
q[++tt] = i;
if(i >= k - 1) cout << a[q[hh]] << ' ';
}
cout << endl;
return 0;
}