单调栈和单调队列

Posted on 2022-02-26 23:04  ZheyuHarry  阅读(78)  评论(0编辑  收藏  举报

    我们在前面已经用数组模拟过栈和队列去执行一些基本操作了,然后呢,我们这里引入单调栈和单调队列的概念

 

    单调栈和单调队列顾名思义我们知道这个栈和这个队列中的元素是单调递增(递减)的,那么这些具体能用来处理哪些问题呢?

    单调栈的应用:单调栈则主要用于 [公式] 解决NGE问题(Next Greater Element),也就是,对序列中每个元素,找到下一个比它大的元素。(当然,“下一个”可以换成“上一个”,“比它大”也可以换成“比他小”,原理不变。)

             还可以用于解决“两元素间所有元素均(不)大/小于这两者”的问题

    为什么能这么用呢,我们来理解一下这个算法:

    我们去遍历输入的序列,每遇到一个新的数字,我们就拿去和栈顶的数字比较,因为要找到第一个大于他的,所以如果栈顶元素不符合,我们就把栈顶元素抛掉,重复直至找到第一个大于他的数字或者栈为空的情况下跳出循环,然后把当前的数字入栈,我们会去想被抛掉的那些位于中间的数字是否在之后会需要呢? 我们这样想,如果我们遇到一个新数字比被抛掉的数字都要小,然后被抛掉的数字又小于当时抛掉他们之后入栈的那个数字,而且显然那个数字比被抛掉的数字在序列中更靠近当前这个新的数字,所以我们可以放心地丢掉它!

    然后就是第二个应用了:

    其实这里还是如同第一个应用一样,我们去单调入栈,我们会发现那些被抛掉的元素小于其两边的值,对于后面没有影响,如果我们的题目是要求我们去找满足这个条件的元素对有多少,我们其实需要的是创建一个pair的栈,分别存储当前数字以及具有当前数字重复了多少个,我们直接去让ans加这些重复数字即可。

    来板子:

    830. 单调栈 - AcWing题库

    

#include<bits/stdc++.h>
#define maxn 100010

using namespace std;
int stk[maxn],t;

int main()
{
std::ios::sync_with_stdio(false) ; std::cin.tie(0);
int n;
cin>>n;
for(int i = 1; i<=n;i++){
int x;
cin>>x;
if(t == 0){
cout<<-1<<" ";
stk[++t] = x;
}
else {
while(t!=0 && stk[t]>= x) t--;
if(t == 0 ) cout<<-1<<" ";
else cout<<stk[t]<<" ";
stk[++t] = x;
}
}
return 0;
}

    P1823 [COI2007] Patrik 音乐会的等待 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

 

#include<bits/stdc++.h>
#define maxn 500010
#define x first
#define y second

using namespace std;
typedef pair<int,int> PII;
int a[maxn],t = 0;
PII stk[maxn];
int main()
{
int n ;
cin>>n;
for(int i = 1 ; i<=n; i++) cin>>a[i];
long long ans = 0;
for(int i = 1 ;i<=n; i++){
int cnt = 0;
while(t>=1 && stk[t].x<=a[i] ){
if(stk[t].x == a[i]) cnt = stk[t].y;
ans+=stk[t].y;
t--;
}
if(t>=1) ans++;
stk[++t].x = a[i];
stk[t].y = cnt+1;
}
cout<<ans<<'\n';
return 0;
}

 

//这个是讲单调栈的一个知乎贴,可以看看 算法学习笔记(67): 单调栈 - 知乎 (zhihu.com)

 

     好了,接下来上场的就是我们的单调队列了

    如果一个选手比你小还比你强,你就可以退役了。”——单调队列的原理

   主要应用:好久没写笔记了,先补一个简单的。单调队列是一种主要用于解决滑动窗口类问题的数据结构,即,在长度为 [公式] 的序列中,求每个长度为 [公式] 的区间的区间最值。它的时间复杂度是 [公式] ,在这个问题中比 [公式] 的ST表线段树要优。

    单调队列的基本思想是,维护一个双向队列(deque),遍历序列,仅当一个元素可能成为某个区间最值时才保留它。

     怎么具体来实现这个算法呢,我们再来看一下,我们定义一个双向队列,用hh和tt分别表示头指针和尾指针,每当我们遍历到一个新元素时,(先假设我们在找滑动窗口中的最小值),如果栈不为空的话,我们从栈尾开始与这个元素进行比较,如果栈中的元素大于等于这个新元素时,我们可以让他直接出栈 , 因为随着窗口的滑动 ,当这个新元素加进来之后,栈内的这个元素就没有了成为最小值的可能性,因为随着栈的移动,当栈内元素存在时,总存在这个新元素比他小,因此我们可以我发现我们所维护的双端队列是单调递增的,因为当前面的更小的值滑出窗口外后,新元素仍然有成为最小值的机会(成为最优秀的机会是由后来的人决定的,前面的人会渐行渐远,不再会影响我们:只要别人够烂,你就是牛逼的那一个),对于找最大值亦是同理。

     然后,有些细节我需要讲一下,在这里我们的单调队列是以下标的方式来进行指向的,而且我们所找的最值是hh指针所指向的元素,并且随着窗口的移动,在对新元素进行比较之前,我们要及时地将hh移动一位(大四学长可以滚蛋了hh),然后注意一下何时输出即可!

 

    上板子:

    154. 滑动窗口 - AcWing题库

    

#include<bits/stdc++.h>
#define maxn 1000010

 

using namespace std;
int q[maxn],a[maxn];

 

int main()
{
std::ios::sync_with_stdio(false) ; std::cin.tie(0);
int n,k;
cin>>n>>k;
for(int i = 1;i<=n;i++) cin>>a[i];
int hh = 0, tt = -1;
for(int i = 1 ;i<=n;i++){
if(hh<=tt && q[hh]<i-k+1) hh++;
while(tt>=hh && a[q[tt]] >= a[i]) tt--;
q[++tt] = i;
if(i>k-1) cout<<a[q[hh]]<<" ";
}
cout<<'\n';
hh = 0, tt = -1;
for(int i = 1 ;i<=n;i++){
if(hh<=tt && q[hh]<i-k+1) hh++;
while(tt>=hh && a[q[tt]] <= a[i]) tt--;
q[++tt] = i;
if(i>k-1) cout<<a[q[hh]]<<" ";
}
return 0;
}