栈与队列
栈(先进后出)
栈的一个重要特性就是先进后出,
- 数组模拟栈
模板
// tt表示栈顶
int stk[N], tt = 0;
// 向栈顶插入一个数
stk[ ++ tt] = x;
// 从栈顶弹出一个数
tt -- ;
// 栈顶的值
stk[tt];
// 判断栈是否为空
if (tt > 0)
{
}
例题
#include <iostream>
using namespace std;
const int N=1e5+10;
int m;
int stk[N],tt;
int main(){
cin>>m;
while(m--){
string op;
cin>>op;
if (op=="push")cin>>stk[++tt];
else if(op=="pop")tt--;
else if(op=="empty")cout<<(tt?"NO":"YES")<<endl;
else cout<<stk[tt]<<endl;
}
return 0;
}
队列(先进先出)
解析
模板
- 普通队列
// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;
// 向队尾插入一个数
q[ ++ tt] = x;
// 从队头弹出一个数
hh ++ ;
// 队头的值
q[hh];
// 判断队列是否为空
if (hh <= tt)
{
}
- 循环队列
// hh 表示队头,tt表示队尾的后一个位置
int q[N], hh = 0, tt = 0;
// 向队尾插入一个数
q[tt ++ ] = x;
if (tt == N) tt = 0;
// 从队头弹出一个数
hh ++ ;
if (hh == N) hh = 0;
// 队头的值
q[hh];
// 判断队列是否为空
if (hh != tt)
{
}
例题
#include <iostream>
using namespace std;
const int N=1e5+10;
int a[N],hh=0,tt=-1;
int main(){
int n;
cin>>n;
while(n--){
string op;
cin>>op;
int x;
if(op=="push"){
cin>>x;
a[++tt]=x;
}else if(op=="pop"){
hh++;
}else if(op=="empty"){
if(tt<hh)cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}else{
cout<<a[hh]<<endl;
}
}
return 0;
}
单调栈和单调队列
单调栈
单调栈对应题型——给定一个序列,求序列中每一个数左(右)边,离它最近的且比它大(小)的数。
模板&例题
很容易能想到的暴力做法,使用两重循环,对每一个数向左进行查找,找到的第一个比当前数小的就输出找不到就“-1”然后进行下一个循环。当然这样就很容易超时。
可以使用一个栈来保存经过的数字,每次查找的时候就向前出栈找,这样可以发现一个性质,要是存在一个数据段a1,a2,a3,其中a1大于a2,那么a2是不需要a1作为答案的而且a3或之后的数也不需要a1作为答案,因为要找也找比a1小的a2。这时候我们就可以确定a1是不会作为答案出现的,所以直接出栈就好。
这个的时间复杂度看似有两重循环,但是一重循环内的入栈出栈操作每个数只会出现一次,所以实际时间复杂度是O(n)级别的。
#include <iostream>
using namespace std;
const int N=100010;
int stk[N],tt;
int main(){
int n;
scanf("%d",&n);
while(n--){
int x;
scanf("%d",&x);
while(tt&&stk[tt]>=x)tt--;
if(!tt)printf("-1 ");
else printf("%d ",stk[tt]);
stk[++tt]=x;
}
return 0;
}
单调队列
解析
模板&例题
可以使用一个队列来维护,每次一个数入队,要是当前队列长度超过窗口,就将队首元素弹出,以此保证窗口的稳定。
从数据上来看,要是直接暴力跑时间复杂度将会是 O ( 1 0 6 ∗ 1 0 6 ) O(10^6∗10^6) O(106∗106)绝对超时,所以还要想办法优化。和单调栈一样找一个数据段 a 1 , a 2 , a 3 a_1,a_2,a_3 a1,a2,a3,其中 a 1 < a 2 > a 3 a1<a2>a3 a1<a2>a3,只要队列中存在这样的情况,在放入 a 3 a_3 a3 的时候就将在 a 3 a_3 a3 前面并且比 a 3 a_3 a3 小的 a 1 , a 2 a_1,a_2 a1,a2 从队列中删除。
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
值 | 1 | 3 | -1 | -3 | 5 | 3 | 6 | 7 |
① i = 0 i=0 i=0
h h = 0 , t t = − 1 、 hh=0,tt=-1、 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;//q[0]=0;将a[0]放入队列
if(i>=k-1)printf("%d ",a[q[hh]]);//队列尚未第一次满
}
h h = 0 , t t = 0 , q [ h h ] = 0 , q [ t t ] = 0 hh=0,tt=0,q[hh]=0,q[tt]=0 hh=0,tt=0,q[hh]=0,q[tt]=0,队列中: a [ 0 ] a[0] a[0]。
② i = 1 i=1 i=1
h h = 0 , t t = 0 , q [ h h ] = 0 , q [ t t ] = 0 hh=0,tt=0,q[hh]=0,q[tt]=0 hh=0,tt=0,q[hh]=0,q[tt]=0,队列中: a [ 0 ] a[0] a[0]。
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--;//队列非空,队尾大于等于当前值a[0]>=a[1]不成立,即队尾小于当前值,跳出。
q[++tt]=i;//q[1]=1;将a[1]放入队列
if(i>=k-1)printf("%d ",a[q[hh]]);//队列尚未第一次满
}
h h = 0 , t t = 1 , q [ h h ] = 0 , q [ t t ] = 1 hh=0,tt=1,q[hh]=0,q[tt]=1 hh=0,tt=1,q[hh]=0,q[tt]=1,队列中: a [ 0 ] , a [ 1 ] a[0],a[1] a[0],a[1]。
③ i = 2 i=2 i=2
h h = 0 , t t = 1 , q [ h h ] = 0 , q [ t t ] = 1 hh=0,tt=1,q[hh]=0,q[tt]=1 hh=0,tt=1,q[hh]=0,q[tt]=1,队列中: a [ 0 ] , a [ 1 ] a[0],a[1] a[0],a[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--;//队列非空,队尾大于等于当前值a[1]>=a[2]成立,即队尾大于当前值,删除队尾,再次判断,仍然小于,继续执行,直到队列为空或遇到更小的,tt=-1。
q[++tt]=i;//q[0]=2;将原来的a[0]覆盖,同时原来的a[1]也失效
if(i>=k-1)printf("%d ",a[q[hh]]);//队列已满开始输出,输出a[q[hh]],即输出a[2]。
}
h h = 0 , t t = 0 , q [ h h ] = 2 , q [ t t ] = 2 hh=0,tt=0,q[hh]=2,q[tt]=2 hh=0,tt=0,q[hh]=2,q[tt]=2,队列中: a [ 2 ] a[2] a[2]。
看到这一步就已经清晰了,每次while判断都会将最小的数放在最前面,因为是单调的即使窗口划出也是同样满足当前的队首也是最小值的。
④ i = 3 i=3 i=3
h h = 0 , t t = 0 , q [ h h ] = 2 , q [ t t ] = 2 hh=0,tt=0,q[hh]=2,q[tt]=2 hh=0,tt=0,q[hh]=2,q[tt]=2,队列中: a [ 2 ] a[2] a[2]。
for(int i=0;i<n;i++){
if(hh<=tt&&i-k+1>q[hh])hh++;//队列非空,队首下标未超出,跳过(注意这里是q[hh],不是hh。即当前队列中的元素还有可以输出的,要是出现-3,-2,-1,0这样的情况,当窗口为3,要滑动到-2,-1,0的时候就要限定-3已经超过窗口了,这时就体现了这句的意义)。
while(hh<=tt&&a[q[tt]]>=a[i])tt--;//队列非空,队尾大于等于当前值a[2]>=a[3]成立,即队尾大于当前值,删除队尾,tt=-1。
q[++tt]=i;//q[0]=3;
if(i>=k-1)printf("%d ",a[q[hh]]);//队列已满开始输出,输出a[hh],即输出a[3]。
}
h h = 0 , t t = 0 , q [ h h ] = 3 , q [ t t ] = 3 hh=0,tt=0,q[hh]=3,q[tt]=3 hh=0,tt=0,q[hh]=3,q[tt]=3,队列中: a [ 3 ] a[3] a[3]。
当然选出最大值只要更改while判断的逻辑就行了。
- AC代码
#include <iostream>
using namespace std;
const int N=1e6+10;
int n,k;
int a[N],q[N];
int main(){
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++)scanf("%d",&a[i]);
int hh=0,tt=-1;//hh:队头,tt:队尾.
for(int i=0;i<n;i++){
//队列是否为空、队头是否已经划出窗口(队列中存的是下标)
//因为每次窗口只会移动一位,所以不用while。
if(hh<=tt&&i-k+1>q[hh])hh++;
//不为空、当前队列里的值小于大于a[i]的都出去,(a[]是单调递增的,下面是单调递减)
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;
}