首页 > 其他分享 >单调队列(例题详解+模板cpp)

单调队列(例题详解+模板cpp)

时间:2023-04-21 12:08:22浏览次数:49  
标签:head nums 队列 元素 tail 单调 cpp 例题 模板


有一类问题需要维护一段区间内的最大值或最小值,例如滑动窗口区间最值等问题。一般情况下,我们可以使用线段树、ST表等数据结构来解决这类问题,但是这些数据结构的实现较为复杂,需要一定的时间和精力来学习和掌握。而单调队列则是一个简单而高效的数据结构,可以用来解决这类问题。

基本概念

单调队列是一种存储数据的队列,其中元素的顺序是单调递增或单调递减的。在算法竞赛中,我们一般使用两个单调队列,一个维护单调递增序列,另一个维护单调递减序列。

单调队列是一个双端队列

单调队列的应用:

滑动窗口

滑动窗口是一类问题,需要在一个长度为n的序列中,找到所有长度为k的连续子序列中的最大值或最小值。使用单调队列可以在O(n)的时间复杂度内解决该问题。

具体做法如下:

  1. 首先,将前k个元素插入单调队列中,并记录队列的最大值或最小值。
  2. 然后,从第k+1个元素开始,每次将一个新的元素插入单调队列中。
  3. 插入时,先将队列中所有小于等于该元素的队尾元素出队,保证队列中的元素单调递减。
  4. 然后,将该元素插入队尾,并记录队列的最大值或最小值。
  5. 最后,将长度为k的子序列的最大值或最小值输出即可。

区间最值

需要在一个长度为n的序列中,找到所有长度为k的子序列中的最大值或最小值。使用单调队列可以在O(n)的时间复杂度内解决该问题。

其实现方法与上面类似,但是需要注意:

  • 区间最值问题是在不限制子序列连续性的情况下,找到子序列中的最大值或最小值。
  • 而滑动窗口问题则是在限制子序列必须连续的情况下,找到所有长度为k的子序列中的最大值或最小值。

154. 滑动窗口

154. 滑动窗口 - AcWing题库

题目要求:给你一段长度为n的序列,并且给你一个k,使得你维护一个长度为k的子序列,并且找到整个序列中全部长度为k的子序列的所有的最小值与最大值。

示例:

8 3
1 3 -1 -3 5 3 6 7

结果如下:

min: -1 -3 -3 -3 3 3
max:  3  3  5  5  6  7

如果我们在使用两重for循环,如下所示:

for (int i=1;i<=n;i++){
	int num=0;
	for (int j=i-k+1;j<=i;j++){
		num=max(num,nums[j]);
	}
	ans=max(ans,num);
}

这样每次都重复枚举新的i的位置,并且找到其对应窗口的最大值与最小值,则时间复杂度为O(nk)。我们可以使用单调队列使得时间复杂度降低为O(nlogn):

单调队列的过程如下:以维护滑动窗口内的最大值为例

head:单调队列头 ;tail:单调队列尾

q:单调队列存储数组,nums:原数组

k=3

注意我们的单调队列存储的是元素nums[i]的下标i

首先head与tail初始化为0(下标1表示起始元素)

  1. i=1,元素 1 :此时队列为空,直接插入单调队列的尾部,tail++,此时 head=0,tail=1,q: 1
  2. i=2,元素 3 :此时队列非空,可以注意到队尾元素 1 小于3,因为我们的单调队列维护最大值,所以会删除队尾元素,一直到待插入元素小于队尾元素才行,或者队列为空,tail–,然后我们找到了合适的位置,则元素 3 插入到单调队列的尾部,++tail;由于我们先出再入,此时 head=0,tail=1,q:2
  3. i=3,元素 -1:此时队列非空,并且待插入元素小于队尾元素,直接插入到尾部,tail++,并且此时枚举到的元素下标为 i= 3 ,此时正好是第一个长度为k的窗口,因此 head++。此时head=1,tail=2,q:2 -1输出队头元素:nums[q[head]]=nums[q[1]]=nums[2]=3
  4. i=4,元素 -3:此时队列非空,并且待插入元素小于队尾元素,直接插入到尾部,tail++,此时head=1,tail=3,q:2 3 4输出队头元素:nums[q[head]]=nums[q[1]]=nums[2]= 3
  5. i=5,元素 5 :此时队列非空,可以注意到队尾元素小于待插入元素,所以队尾元素出队,一直到待插入元素小于队尾元素才行,或者队列为空,此时我们找到了合适的位置,则元素 5 插入到单调队列的尾部;经过了很多次的tail–后,然后++tail,此时 head=1,tail=1,q:5输出队头元素:nums[q[head]]=nums[q[1]]=nums[5]=5
  6. i=6,元素 3:此时队列非空,并且待插入元素小于队尾元素,直接插入到尾部,tail++,此时head=1,tail=2,q:5 6。输出队头元素:nums[q[head]]=nums[q[1]]=nums[5]=5
  7. i=7,元素 6 :此时队列非空,可以注意到队尾元素小于待插入元素,所以队尾元素出队,一直到待插入元素小于队尾元素才行,或者队列为空,此时我们找到了合适的位置,则元素 6 插入到单调队列的尾部;经过了很多次的tail–后,然后++tail,此时 head=1,tail=1,q:7输出队头元素:nums[q[head]]=nums[q[1]]=nums[7]=6
  8. i=8,元素 7 :此时队列非空,可以注意到队尾元素小于待插入元素,所以队尾元素出队,一直到待插入元素小于队尾元素才行,或者队列为空,此时我们找到了合适的位置,则元素 7 插入到单调队列的尾部;经过了很多次的tail–后,然后++tail,此时 head=1,tail=1,q:8输出队头元素:nums[q[head]]=nums[q[1]]=nums[8]=7

可以注意到几个问题?

  • 我们在单调队列中存储的是原数组的下标值,这样便于找到原始数据以及对队列进行操作
  • q[head] 表示 队头元素,这个元素是一个下标,通过**nums[q[head]]**就可以找到队头的真正数据。
  • tail什么时候变我们都知道,但是什么时候head会变?
  • 当队头元素小于 i-k+1的时候。 i为窗口右端点时,i-k+1表示左端点队头元素表示的是下标,也就是说当 队列队头元素小于窗口的左端点时,说明这个窗口已经超过了,不包含队头元素了,因此需要移动head到窗口的新的左端点位置,head++。
  • 什么时候输出区间最大值?
  • 当遍历下标 i>k-1的时候。k-1表示窗口的最小保持长度,例如 k=3,k-1=2,因此当 i=3的时候,i>k-1,那么表示我们正处于第一个窗口的位置(i=3,窗口包括i=1,2,3位置的元素),那么我们输出队头元素所代表的元素值。往后每遍历一个i,都需要输出一次队头,因为每次都是一个长度为k的子窗口。

同理使用STL的deque也可以实现,因为deque本身就是一个双端队列。

#include <iostream>
#include <string>
#include <cstring>
#include <algorithm>
#include <deque>
using namespace std;
#define int long long
const int N=1e6+10;
int nums[N];
int n,k;
int q[N];
signed main(){
    cin>>n>>k;
    for (int i=1;i<=n;i++){
        cin>>nums[i];
    }
    deque<int> deq;
    //最小值
    for (int i=1;i<=n;i++){
        //如果队列头超过了范围,则弹出队列头
        if (!deq.empty() && deq.front()<i-k+1) deq.pop_front();
        //如果待插入元素比队尾元素小,则一直找到不小的或者队列为空时插入
        while (!deq.empty() && nums[i]<=nums[deq.back()]) deq.pop_back();
        deq.push_back(i);
        if (i>k-1) printf("%lld ",nums[deq.front()]);
    }
    cout<<endl;
    int tail=0,head=0;
    //最大值
    for (int i=1;i<=n;i++){
        //如果队头元素超过了窗口的范围,则队头出队
        if (head<=tail && q[head]<i-k+1) head++;
        //队列不为空,并且要插入的元素大于等于队列尾元素,则寻找一个不大于等于的位置,这些队尾元素都出队;或者队列为空时插入
        while (head<=tail && nums[i]>=nums[q[tail]]) tail--;
        q[++tail]=i;
        //对每一个窗口输出队头元素
        if (i>k-1) printf("%lld ",nums[q[head]]);
    }
    return 0;	
}

135. 最大子序和

135. 最大子序和 - AcWing题库

题目要求:给你一个序列,在长度不超过m的情况下,找到所有长度不超过m的子序列的和的最大值是多少。

示例:

6 4
1 -3 5 1 -2 3

m=4,可以得到最后四个数字的和最大,并且长度为4,不超过m,满足。


一看到子序列的区间和我们就可以想到前缀和

因此这道题目第一个任务就完成了,我们只需要求出前缀和,并且计算在长度不超过m的情况下:

  • 计算sum[i]-sum[j-1]的最大值

其中 i为m长度子序列的右端点,j为左端点,因此sum[i]-sum[j-1]表示的就是这段区间的和

那么现在问题来了,我们如果求得者每个这样的区间的最大值呢,可以使用O(N^2)枚举所有的子区间,然后分别计算他们的最大值,不过这样做显然是超时的。

观察这个式子:要想使得sum[i]-sum[j-1]最大,因此我们不妨固定住右端点i

然后在 [i-m+1,i] 的范围内找到一个最小的前缀和使得 sum[j-1]最小,这样就可以让 sum[i]-sum[j-1]最大。

因此问题就转换为了在 [i-m+1,i]区间内维护一个区间最小值,然后每次更新右端点 i 的时候直接计算sum[i]-sum[j-1]即可,其中 j 的位置就是前缀和最小值的位置

因此我们可以使用单调队列维护滑动窗口最小值.

维护最小值:

  1. 入队的操作:如果队列为空,则插入到队列尾;如果队尾元素大于待插入元素,则弹出队尾元素,一直到队尾元素小于待插入元素位置。
  2. 出队的操作:当队头超过了长度为 m 的窗口的范围,即q[head]在i-m+1的左边,head++

需要注意的点:

  • 出队的时候我们的head需要指向窗口左端点的前一个,因为我们的前缀和的计算是 sum[i]-sum[j-1],j的位置就是左端点的位置。
#include <iostream>
#include <string>
#include <algorithm>
#include <climits>
using namespace std;
const int N=3e5+10;
int n,m,nums[N],sum[N];
signed main(){
    cin>>n>>m;
    for (int i=1;i<=n;i++){
        scanf("%d",&nums[i]);
        sum[i]=sum[i-1]+nums[i];
    }
    int ans=INT_MIN;
    int head=0,tail=0,q[N];
    for (int i=1;i<=n;i++){
        //在[i-m+1,i]的范围内找前缀和的最小值,为j,使得 sum[i]-sum[j-1]最大
        if (head<=tail && q[head]<i-m){
            head++;
        }
        ans=max(ans,sum[i]-sum[q[head]]); //每一个sum[i]-当前窗口最小值= MAXval
        while (head<=tail && sum[i]<=sum[q[tail]]){
            tail--;
        }
        q[++tail]=i;//存储的是下标i
    }
    cout<<ans;
    return 0;
}


标签:head,nums,队列,元素,tail,单调,cpp,例题,模板
From: https://blog.51cto.com/u_15744744/6212409

相关文章

  • Trie字典树(例题详解+模板cpp)
    字典树(Trie树)一:概念字典树是一种树形结构,用于存储一组字符串,支持快速的字符串查找和前缀匹配。字典树的本质是利用字符串之间的公共前缀,将具有相同前缀的字符串合并在一起,从而实现高效的字符串操作。数据结构字典树是一棵多叉树,每个节点包含若干个指向子节点的指针,每个节点代表一......
  • 图的最短路问题(综合详解!!!看这一篇就够了)(spfa-Dijkstra-floyd-模板代码c-)
    文章目录图论:三种最短路及模板模板SPFA算法Floyd算法Dijkstra算法例题与应用反向建边最短路计数1488.最短距离3305.作物杂交4074.铁路与公路图论:三种最短路及模板注意:在这三种算法中我均使用的链式前向星存图,具体方式请看我这篇博客:链式前向星存图详解模板SPFA算法spfa是优化......
  • bfs与dfs详解(经典例题 + 模板c-代码)
    文章目录模板+解析dfsbfs1562.微博转发3502.不同路径数165.小猫爬山模板+解析DFS(深度优先搜索)和BFS(广度优先搜索)是图论中两个重要的算法。dfs其中DFS是一种用于遍历或搜索树或图的算法,BFS则是一种用于搜索或遍历树或图的算法。两种算法都有其自身的优点和缺点,应用于不同的场景中......
  • 最小生成树详解-模板
    概念最小生成树的定义在一张带权无向图中,最小生成树是一棵生成树,它的边权值之和最小。生成树是一颗包含原图中所有顶点的树,它的边集合是原图的一个子集,且任意两个顶点之间都有且仅有一条简单路径。最小生成树的算法目前,最常用的两种最小生成树算法是Kruskal算法和Prim算法。Kruska......
  • 并查集及其扩展(附例题与完整讲解cpp)
    文章目录基础并查集1.P1551亲戚2.P1536村村通种类并查集1.P1892[BOI2003]团伙2.P1525[NOIP2010提高组]关押罪犯3.P2024[NOI2001]食物链带权并查集基础并查集并查集就是用来维护一些元素之间的关系的集合。例如A的亲戚是B,则我们可以把A与B放到同一个集合中,表示AB属......
  • 二分查找例题与模板(蓝桥杯复习+例题讲解+模板c++)
    文章目录二分模板1460.我在哪?102.最佳牛围栏113.特殊排序二分模板本文所使用的二分模板都是确保最终答案落在[L,R]以内,循环以L==R结束,每次二分的中间值会使mid成为左右区间的二者之一。单调递增序列找大于等于x的最小的值:区间的划分[l,mid][mid+1,r]while(l<r){ intmid......
  • 第三章部分例题(6)
    例3-13值传递与引用传递的比较设计思路:通过函数对数值进行改变观察值传递与应用传递后原数值的变化代码:#include<iostream>#include<iomanip>usingnamespacestd;voidfiddle(intin1,int&in2){in1+=100;in2+=100;cout<<"Thevaluesare";cout<......
  • Bootstrap模板-使用现成的免费完善模板制作网页.
    Bootstrap有一系列现成的免费而优秀的模板,我们可以用于制作前端页面稍加改进就是一个美观的页面  模板代码(源自purpleTemplate):<!DOCTYPEhtml><htmllang="en"><head><metacharset="utf-8"><metaname="viewport"content="width=dev......
  • 【第一章 web入门】afr_3——模板注入与proc文件夹
    【第一章web入门】afr_3——模板注入与proc文件夹题目来源n1book,buu上的环境看题url中提供了name参数,类似在路径中进行了文件名查询然后展示:随便输入一个数字:说明肯定题目要求我们利用这个文件读取漏洞。但是输入flag之后显示nopermission。所以尝试其他方法。proc......
  • nyoj 坦克大战 284 (bfs) 模板
    坦克大战1000ms |          内存限制:655353Manyofushadplayedthegame"Battlecity"inourchildhood,andsomepeople(likeme)evenoftenplayitoncomputernow.Whatwearediscussingisasimpleeditionofthisgame.Givena......