0
点赞
收藏
分享

微信扫一扫

单调队列学习笔记(~超详细)


文章目录

  • ​​概念​​
  • ​​例题引入​​
  • ​​暴力解法​​
  • ​​优先队列优化解法​​
  • ​​单调队列(正片开始)​​
  • ​​单调队列解决例题​​
  • ​​总结​​

概念

顾名思义,单调队列的重点分为 “单调” 和 “队列”,“单调” 指的是元素的的 “规律”——递增(或递减),“队列” 指的是元素只能从队头和队尾进行操作(注意,是可以在队尾进行操作的,这和正常的队列是有一定区别的,我们待会再说为什么需要这样做。)。
这种数据结构通常用于滑动窗口区间最值问题。

例题引入

给出一个长度为单调队列学习笔记(~超详细)_单调队列的数组,编程输出每单调队列学习笔记(~超详细)_最小值_02个连续的数中的最大值和最小值。

暴力解法

我们拿到这题,最暴力的想法很简单,对于每一段单调队列学习笔记(~超详细)_最小值_03的序列,逐个比较来找出最大值(和最小值)。
暴力代码:

/*
*
*/
#include<bits/stdc++.h> //POJ不支持

#define rep(i,a,n) for (int i=a;i<=n;i++)//i为循环变量,a为初始值,n为界限值,递增
#define per(i,a,n) for (int i=a;i>=n;i--)//i为循环变量, a为初始值,n为界限值,递减。
#define pb push_back
#define IOS ios::sync_with_stdio(false);cin.tie(0); cout.tie(0)
#define fi first
#define se second
#define mp make_pair

using namespace std;

const int inf = 0x3f3f3f3f;//无穷大
const int infs = -0x3f3f3f;//无穷小
const int maxn = 1e5;//最大值。
typedef long long ll;
typedef long double ld;
typedef pair<ll, ll> pll;
typedef pair<int, int> pii;
//*******************************分割线,以上为自定义代码模板***************************************//

int nums[maxn];
int n,k;
int main(){
//freopen("in.txt", "r", stdin);//提交的时候要注释掉
IOS;
while(cin>>n>>k){
for(int i=1;i<=n;i++)cin>>nums[i];
int maxx,minn;
for(int i=1;i<=n;i++){
maxx=infs,minn=inf;
for(int j=i;j<=i+k-1;j++){
if(nums[j]>maxx)maxx=nums[j];
if(nums[j]<minn)minn=nums[j];
}
cout<<maxx<<" "<<minn<<endl;
}
}
return 0;
}

这个算法时间复杂度约为 单调队列学习笔记(~超详细)_#define_04 。很显然,这其中进行了大量重复工作,除了开头单调队列学习笔记(~超详细)_i++_05 个和结尾 单调队列学习笔记(~超详细)_i++_05 个数之外,每个数都进行了 单调队列学习笔记(~超详细)_最小值_07 次比较,而题中若单调队列学习笔记(~超详细)_单调队列_08的数据范围给的很大 ,当单调队列学习笔记(~超详细)_最小值_07稍大的情况下,显然会TLE。

优先队列优化解法

我们知道,对于优先队列而言,它会自动排序,我们利用两个优先队列自定义优先级,获取当前队列中的最大值序号和最小值序号。我们具体看代码。

/*

*。
*/
#include<bits/stdc++.h> //POJ不支持

#define rep(i,a,n) for (int i=a;i<=n;i++)//i为循环变量,a为初始值,n为界限值,递增
#define per(i,a,n) for (int i=a;i>=n;i--)//i为循环变量, a为初始值,n为界限值,递减。
#define pb push_back
#define IOS ios::sync_with_stdio(false);cin.tie(0); cout.tie(0)
#define fi first
#define se second
#define mp make_pair

using namespace std;

const int inf = 0x3f3f3f3f;//无穷大
const int infs = -0x3f3f3f;//无穷小
const int maxn = 1e5;//最大值。
typedef long long ll;
typedef long double ld;
typedef pair<ll, ll> pll;
typedef pair<int, int> pii;
//*******************************分割线,以上为自定义代码模板***************************************//

int nums[maxn];
int maxx[maxn],minn[maxn];//存储最大值和最小值。
int n,k;
//注意比较函数对象的写法,自定义优先级需要反着来,可以参考greater<int> 就是从小到大排序,说明反着来的,这里不做解释。
struct cmp1{
bool operator()(int i,int j){
//从大到小排序
return nums[i]<nums[j];
}
};
struct cmp2{
bool operator()(int i,int j){
//从小到大排序
return nums[i]>nums[j];
}
};
int main(){
//freopen("in.txt", "r", stdin);//提交的时候要注释掉
IOS;
while(cin>>n>>k){
for(int i=1;i<=n;i++)cin>>nums[i];
int cnt=0;
priority_queue<int,vector<int>,cmp1> q_max;//维护最大值优先队列
priority_queue<int,vector<int>,cmp2> q_min;//维护最小值优先队列
//先将前k个值入队。
for(int i=1;i<=k;i++){
q_max.push(i);
q_min.push(i);
}
//取队头元素存储
maxx[cnt]=nums[q_max.top()];
minn[cnt]=nums[q_min.top()];
cnt++;
//接下来开始遍历接下来的元素。
for(int i=k+1;i<=n;i++){
q_max.push(i);
q_min.push(i);
//判断队头元素是否过期。
while(i-q_max.top()>=k)
q_max.pop();
//存储
maxx[cnt]=nums[q_max.top()];
while(i-q_min.top()>=k)
q_min.pop();
minn[cnt]=nums[q_min.top()];
cnt++;
}
for(int i=0;i<cnt;i++){
cout<<maxx[i]<<" "<<minn[i]<<endl;
}
}
return 0;
}

这种方法代码比较长,但却很高效,我们来分析一下,优先队列每次加入新元素时都需要单调队列学习笔记(~超详细)_#define_10的时间来调整堆。那么这个算法平均时间复杂度为单调队列学习笔记(~超详细)_单调队列_11,和第一种方法相比,确实优化了不少。

单调队列(正片开始)

用优先队列处理这道题确实有着不错的表现,但仍没有达到一个线性时间单调队列学习笔记(~超详细)_i++_12。而有了上面单调队列的概念,我们自然会想到用单调队列优化:

这题目要求的是每连续的 个数中的最大(最小)值,很明显,我们以查找小值为例:当一个数进入所要 “寻找” 最小值的范围中时,若这个数比其前面(先进队)的数要小,显然,前面的数会比这个数先出队且不再可能是最小值

也就是说——当满足以上条件时,可将前面的数 “弹出”,再将该数真正 push 进队尾。这就相当于维护了一个递增的队列,符合单调队列的定义,减少了重复的比较次数,不仅如此,由于维护出的队伍是查询范围内的且是递增的,队头必定是该查询区域内的最小值,因此输出时只需输出队头即可

显而易见的是,在这样的算法中,每个数只要进队与出队各一次,因此时间复杂度被降到单调队列学习笔记(~超详细)_i++_12了 。而由于查询区间长度是固定的,超出查询空间的值再大也不能输出,因此还需要 单调队列学习笔记(~超详细)_队列_14 数组记录第 单调队列学习笔记(~超详细)_队列_15个队中的数在原数组中的位置,以弹出越界的队头。

这样是不是有点懵,没事,我们来看一个例子,看看单调队列是怎么实现这个操作的。

假设原序列为:

1 3 -1 -3 5 3 6 7
设我们的单调队列学习笔记(~超详细)_最小值_02为3

因为我们始终要维护队列保证递增的状态,所以这个队列在操作过程中会是这样:

操作

队列状态

当前区间和最小值

1 入队

{1}

单调队列学习笔记(~超详细)_最小值_17

3 比 1 大,3 入队

{1 3}

单调队列学习笔记(~超详细)_i++_18

这里注意我们快要到达窗体限制长度,接下来我们要记录我们取出来的最小值和下标,及时剔除过期元素。

-1 比队列中所有元素小,所以清空队列 -1 入队

{-1}

单调队列学习笔记(~超详细)_i++_19

-3 比队列中所有元素小,所以清空队列 -3 入队

{-3}

单调队列学习笔记(~超详细)_队列_20

5 比 -3 大,直接入队

{-3 5}

单调队列学习笔记(~超详细)_#define_21

3 比 5 小,5 出队,3 入队

{-3 3}

单调队列学习笔记(~超详细)_i++_22

-3 已经在窗体外,所以 -3 出队;6 比 3 大,6 入队

{3 6}

单调队列学习笔记(~超详细)_最小值_23

7 比 6 大,7 入队

{3 6 7}

单调队列学习笔记(~超详细)_单调队列_24

相信我们经过这个过程之后,大概知道利用单调队列的工作流程了,实现单调队列总共有两种方法,一种是利用数组实现,我们在数组上进行表面的入队和出队操作,这种我们就不受普通队列的限制,我们可以自定义。那么如果我们要使用STL中的队列,由于我们要在两端进行操作,故我们会选择双端队列deque来实现,接下来就介绍两种实现方法解决我们的例题。

单调队列解决例题

数组实现单调队列

/*

*/
#include<bits/stdc++.h> //POJ不支持

#define rep(i,a,n) for (int i=a;i<=n;i++)//i为循环变量,a为初始值,n为界限值,递增
#define per(i,a,n) for (int i=a;i>=n;i--)//i为循环变量, a为初始值,n为界限值,递减。
#define pb push_back
#define IOS ios::sync_with_stdio(false);cin.tie(0); cout.tie(0)
#define fi first
#define se second
#define mp make_pair

using namespace std;

const int inf = 0x3f3f3f3f;//无穷大
const int infs = -0x3f3f3f;//无穷小
const int maxn = 1e5;//最大值。
typedef long long ll;
typedef long double ld;
typedef pair<ll, ll> pll;
typedef pair<int, int> pii;
//*******************************分割线,以上为自定义代码模板***************************************//

int n,k,nums[maxn],q[maxn],pos[maxn];//q为我们的单调队列,pos数组记录队中元素下标,为了剔除过期元素。
int st,ed;//表示队头和队尾
void get_min(){
//先将前k-1个入队。
st=1,ed=0;
for(int i=1;i<k;i++){
while(st<=ed&&q[ed]>=nums[i])ed--; //维护单调递增序列,和队尾元素进行比较
ed++;
q[ed]=nums[i];//获取当前的队尾元素。
pos[ed]=i;//记录每个元素的下标。
}
for(int i=k;i<=n;i++){
while(st<=ed&&q[ed]>=nums[i])ed--;
ed++;
q[ed]=nums[i];
pos[ed]=i;
//关键一步,取队头元素。
while(pos[st]<i-k+1)st++;//队头元素过期,出队。
cout<<q[st]<<" ";
}
cout<<endl;
}
//和求最小值同样的解法,只不过现在是要维护单调递减序列。
void get_max(){
//先将前k-1个入队。
st=1,ed=0;
for(int i=1;i<k;i++){
while(st<=ed&&q[ed]<=nums[i])ed--; //维护单调递减序列,和队尾元素进行比较
ed++;
q[ed]=nums[i];//获取当前的队尾元素。
pos[ed]=i;//记录每个元素的下标。
}
for(int i=k;i<=n;i++){
while(st<=ed&&q[ed]<=nums[i])ed--;
ed++;
q[ed]=nums[i];
pos[ed]=i;
//关键一步,取队头元素。
while(pos[st]<i-k+1)st++;//队头元素过期,出队。
cout<<q[st]<<" ";
}
cout<<endl;
}
int main(){
//freopen("in.txt", "r", stdin);//提交的时候要注释掉
IOS;
while(cin>>n>>k){
for(int i=1;i<=n;i++){
cin>>nums[i];
}
get_min();
get_max();
}
return 0;
}

STL双端队列实现

这里要注意的就是我们没必要开一个数组来存储下标了,我们直接将下标入队,通过下标直接可以判断队头元素是否过期,这样更快,具体看代码。一定要细心,因为双端队列的函数较多。

/*

*/
#include<bits/stdc++.h> //POJ不支持

#define rep(i,a,n) for (int i=a;i<=n;i++)//i为循环变量,a为初始值,n为界限值,递增
#define per(i,a,n) for (int i=a;i>=n;i--)//i为循环变量, a为初始值,n为界限值,递减。
#define pb push_back
#define IOS ios::sync_with_stdio(false);cin.tie(0); cout.tie(0)
#define fi first
#define se second
#define mp make_pair

using namespace std;

const int inf = 0x3f3f3f3f;//无穷大
const int infs = -0x3f3f3f;//无穷小
const int maxn = 1e5;//最大值。
typedef long long ll;
typedef long double ld;
typedef pair<ll, ll> pll;
typedef pair<int, int> pii;
//*******************************分割线,以上为自定义代码模板***************************************//

int n,k,nums[maxn],pos;//pos记录队头元素下标,为了剔除过期元素。
int st,ed;//表示队头和队尾
void get_min(){
deque<int> q;//单调递增队列
//先将前k-1个入队。
for(int i=1;i<k;i++){
while(!q.empty()&&nums[q.back()]>=nums[i])q.pop_back();//维护单调递增序列。
q.push_back(i); //我们这里要存储下标。
}
for(int i=k;i<=n;i++){
while(!q.empty()&&nums[q.back()]>=nums[i])q.pop_back();
q.push_back(i);
//关键一步,判断队头元素是否过期
while(!q.empty()&&q.front()<i-k+1)q.pop_front();//过期,删除
cout<<nums[q.front()]<<" ";
}
cout<<endl;
}
//和求最小值同样的解法,只不过现在是要维护单调递减序列。
void get_max(){
deque<int> q;//单调递减队列
//先将前k-1个入队。
for(int i=1;i<k;i++){
while(!q.empty()&&nums[q.back()]<=nums[i])q.pop_back();//维护单调递减序列。
q.push_back(i); //我们这里要存储下标。
}
for(int i=k;i<=n;i++){
while(!q.empty()&&nums[q.back()]<=nums[i])q.pop_back();
q.push_back(i);
//关键一步,判断队头元素是否过期
while(!q.empty()&&q.front()<i-k+1)q.pop_front();//过期,删除
cout<<nums[q.front()]<<" ";
}
cout<<endl;
}
int main(){
freopen("in.txt", "r", stdin);//提交的时候要注释掉
IOS;
while(cin>>n>>k){
for(int i=1;i<=n;i++){
cin>>nums[i];
}
get_min();
get_max();
}
return 0;
}

总结

单调队列的单调性是要我们自己去维护的,这一定得谨记,因为这本来就是我们创建的数据结构,操作都得我们来完成,利用单调队列的情况虽然很少,但如果需要,这能起到意想不到的作用。



举报

相关推荐

0 条评论