前言 | Preface
这几天连续做了好几道单调队列的题,难度从绿到蓝不等,摸索出了一些经验,也总结了一些单调队列的特点和规律。
本文作者:Jerrycyx
推荐在洛谷专栏阅读以获得更好的阅读体验。
2025.3.21 更新:同步了例题题解内容。
概述 | Outline
顾名思义,单调队列的重点分为「单调」和「队列」。
「单调」指的是元素的「规律」——递增(或递减)。
「队列」指的是元素只能从队头和队尾进行操作。
——OI Wiki
单调队列(monitonous queue) 是一种很实用的数据结构。如何理解单调队列?首先,“队列”其实是指双端队列,因为单调队列队首队尾都可以进出;其次“单调”是指单调队列里的元素具有单调性(单调递增或递减均可)。
我知道你没看懂。 单调队列光靠这些文字叙述确实不好理解,结合例题就能明白了。
(大佬不想看例题可直接跳转至「总结 | Summary」部分)
例题 | Example
本文重点为单调队列,所以其他算法内容一律从略,且一般会有引用的格式加以标明。
下面有一半的绿题都是我从蓝降下来的。
P1886 滑动窗口 /【模板】单调队列 ★ I m p o r t a n t \tt\textcolor{Orange}{\bigstar Important} ★Important
——本段文字已投稿至该题题解。
老生常谈的单调队列模板题了,题面要求定长区间最值,下面来说单调队列的运作方式(以求取区间最小值为例)。
单调队列的核心思想是:“老而更劣的元素永远不可能成为最值”(让我想起了我的 OI 生涯)。
例如:在从左往右滑动窗口求最小值时,考虑右侧新加入一个元素 a j a_j aj 时会发生什么。假设区间中已经有的一个元素 a i a_i ai 使得 a i ≥ a j a_i \ge a_j ai≥aj,那么 a j a_j aj 离开窗口一定比 a i a_i ai 要晚,且今后 a i a_i ai 在队列里的时候 a j a_j aj 也一直在队列里。在 a i a_i ai 剩下的生命里直到离开区间, a j a_j aj 永远比它小, a i a_i ai 再也不可能成为(唯一的)最小值了。
现实是残酷的,OIer 们是残忍的, a i a_i ai 已经失去了成为(唯一)最大值的机会,OIer 们毫不留情地抛弃掉它,来保证留下的都是有机会成为最大值的。
如此,单调队列保证了其中不存在 a i , a j a_i,a_j ai,aj 使 i < j i<j i<j 且 a i ≥ a j a_i \ge a_j ai≥aj,即对于任意 i < j i<j i<j 都有 a i < a j a_i<a_j ai<aj,换言之,这个单调队列里的元素是单调递增的。
总的来说,单调队列解决这道题(最小值部分)的过程分为两步:
- 加入新的元素时,从队尾踢掉之前所有不小于它的元素,并自己加入队尾
- 从队头移除已经离开窗口的元素
通常情况下上面两步顺序可以任意交换,少数情况下(即新加入的元素可能不在窗口内时)必须按照上面的顺序。对于这道题,顺序可以任意交换。每次完成上面两步以后,队头即为最大值。
附上我常用的模板代码(求定长区间最小值):
int q[N],head,tail; //单调队列记录最小值的位置,方便后面判断某元素是否已经离开窗口
...
head=1,tail=0; //清空队列
for(int i=1;i<=n;i++) //枚举窗口右端
{
while(head<=tail && i-q[head]+1>k) q[head++]=0; //弹出已经离开窗口的元素
while(head<=tail && a[q[tail]]>a[i]) q[tail--]=0; //从队尾踢掉之前所有比当前元素大的数
q[++tail]=i; //当前元素自己加入队尾
if(i>=k) res[i-k+1]=q[head]; //完成以上操作后,队头即为最小值
}
每个元素最多入队出队一次,所以时间复杂度是 O ( n ) O(n) O(n) 线性的。
对于这道题,同时要求最小和最大值。因为 max ( a i ) = − min ( − a i ) \max(a_i) = - \min(-a_i) max(ai)=−min(−ai),即区间最大值等于序列相反数的最小值的相反数,所以我们可以只写一个求最小值的代码,求过区间最小值以后把序列所有元素取相反数再求一遍最小值,然后再取一次相反数就是区间最大值。
代码:
#include<cstdio>
using namespace std;
const int N=1e6+5;
int n,k,a[N];
int ans1[N],ans2[N];
int q[N],head,tail;
void Calc(int res[]) //指针传参,答案计入 res 数组
{
head=1,tail=0; //清空队列
for(int i=1;i<=n;i++) //枚举窗口右端
{
while(head<=tail && i-q[head]+1>k) q[head++]=0; //弹出已经离开窗口的元素
while(head<=tail && a[q[tail]]>a[i]) q[tail--]=0; //从队尾踢掉之前所有比当前元素大的数
q[++tail]=i; //当前元素自己加入队尾
if(i>=k) res[i-k+1]=q[head]; //完成以上操作后,队头即为最小值
}
return;
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
Calc(ans1); //计算滑动窗口最小值位置,答案计入 ans1
for(int i=1;i<=n-k+1;i++)
printf("%d ",a[ans1[i]]);
putchar('\n');
for(int i=1;i<=n;i++)
a[i]=-a[i]; //所有元素取相反数
Calc(ans2); //计算此时的滑动窗口最小值位置,答案计入 ans2
for(int i=1;i<=n-k+1;i++)
printf("%d ",-a[ans2[i]]); //再取一遍相反数即为最大值
return 0;
}
P1725 琪露诺
呵呵,这道题我其实打了个线段树。
单调队列优化动态规划题。
设 f i f_i fi 表示走到位置 i i i 所获得的最大冰冻指数,那么有:
f i = a i + max j = i − R i − L f j f_i = a_i + \max_{j=i-R}^{i-L}f_j fi=ai+j=i−Rmaxi−Lfj
暴力求 max j = i − R i − L f j \max_{j=i-R}^{i-L}f_j maxj=i−Ri−Lfj 时间复杂度 O ( n 2 ) O(n^2) O(n2),会 TLE。但这是在一个定长数组上求最值,几乎是一个裸的单调队列模板,所以只需要对 max j = i − R i − L f j \max_{j=i-R}^{i-L}f_j maxj=i−Ri−Lfj 用单调队列计算来优化就可以做到 O ( n ) O(n) O(n) 了。
这道题会有一些细节处理问题,不过不是重点。
P3800 Power收集
这道题我打了个 ST 表,甚至还写了题解,链接就不放了,不是重点。
引用一下我的题解:
设 f i , j f_{i,j} fi,j 表示走到坐标 ( i , j ) (i,j) (i,j) 的最大 POWER 值,那么有:
f i , j = a i , j + max p = j − t j + t f i − 1 , p f_{i,j} = a_{i,j} + \max_{p=j-t}^{j+t} f_{i-1,p} fi,j=ai,j+p=j−tmaxj+tfi−1,p
max p = j − t j + t f i − 1 , p \max_{p=j-t}^{j+t} f_{i-1,p} maxp=j−tj+tfi−1,p,标准的定长区间最值问题,这一部分用单调队列来计算,即可优化成 O ( n m ) O(nm) O(nm)。
P2216 [HAOI2007] 理想的正方形
正方形大小 n n n 已经确定了,这又是一个定长区间最值问题。
不过这里区间是二维的,我们设 f max ( x , y ) f_{\max}(x,y) fmax(x,y) 表示以 ( x , y ) (x,y) (x,y) 为右下角,边长为 n n n 的正方形区域内元素的最大值,那么有(以下以 a a a 代表矩阵数组):
f max ( x , y ) = max i = x − n + 1 x max j = y − n + 1 y a i , j = max i = x − n + 1 x ( max j = y − n + 1 y a i , j ) f_{\max}(x,y) = \max_{i=x-n+1}^{x} \max_{j=y-n+1}^{y} a_{i,j} = \max_{i=x-n+1}^{x} \left( \max_{j=y-n+1}^{y} a_{i,j} \right) fmax(x,y)=i=x−n+1maxxj=y−n+1maxyai,j=i=x−n+1maxx(j=y−n+1maxyai,j)
我们先在 a a a 上对每一行跑一遍单调队列求出 f max ′ ( x , y ) = max j = y − n + 1 y a i , j f'_{\max}(x,y) = \max_{j=y-n+1}^{y} a_{i,j} fmax′(x,y)=maxj=y−n+1yai,j,再在 f max ′ f'_{\max} fmax′ 上对每一列跑一遍单调队列求出 f max ( x , y ) = max i = x − n + 1 x f max ′ ( i , y ) f_{\max}(x,y) = \max_{i=x-n+1}^{x} f'_{\max}(i,y) fmax(x,y)=maxi=x−n+1xfmax′(i,y)。
对最小值进行同样操作后, max { f max ( i , j ) − f min ( i , j ) } \max\{f_{\max}(i,j)-f_{\min}(i,j)\} max{fmax(i,j)−fmin(i,j)} 即为所求。
P2627 [USACO11OPEN] Mowing the Lawn G
转化一下,将“不能连续选择超过 k k k 头奶牛”转化成“相邻两头不选的奶牛之间间距不超过 k k k”,然后要使选择的奶牛效率值最大,也就是使不选的奶牛效率值最小。
为与“选择”区分,以下以“标记”来代表“不选择”。设 f i f_i fi 表示在标记了第 i i i 头奶牛的情况下,满足“相邻两头被标记的奶牛之间间距不超过 k k k”的条件,前 i i i 头奶牛所获得的最小效率值,那么有:
f i = e i + min j = i − k − 1 i − 1 f j f_i = e_i + \min_{j=i-k-1}^{i-1} f_j fi=ei+j=i−k−1mini−1fj
最终答案为 ∑ i = 1 n e i − f n + 1 \sum_{i=1}^{n}e_i - f_{n+1} ∑i=1nei−fn+1( f f f 算到效率值为 0 0 0 的 n + 1 n+1 n+1 以保证 1 ∼ n 1 \sim n 1∼n 全部满足条件且没有必须标记的要求,方便最后统计答案)
直接计算时间复杂度 O ( n k ) O(nk) O(nk),但是观察到上式 min j = i − k − 1 i − 1 f j \min_{j=i-k-1}^{i-1} f_j minj=i−k−1i−1fj 部分在找 f f f 在区间 [ i − 1 , i − k − 1 ] [i-1,i-k-1] [i−1,i−k−1] 内的最小值,这又是一个定长区间最值问题,直接塞单调队列模板即可优化成 O ( n ) O(n) O(n)。
P2034 选择数字
跟上面那道题一样,不再赘述。
P1419 寻找段落
推荐题解:点击这里,不过单调队列部分不太详细。
最终是要判定是否存在 r − l + 1 ∈ [ s , t ] r-l+1 \in [s,t] r−l+1∈[s,t] 使得 s u m r − s u m l − 1 ≥ 0 sum_r-sum_{l-1} \ge 0 sumr−suml−1≥0。转化一下,存在 l − 1 ∈ [ r − t , r − s ] l-1 \in [r-t,r-s] l−1∈[r−t,r−s] 使得 s u m l − 1 ≤ s u m r sum_{l-1} \le sum_r suml−1≤sumr。即:
min l − 1 ∈ [ r − t , r − s ] s u m l − 1 ≤ s u m r \min_{l-1 \in [r-t,r-s]} sum_{l-1} \le sum_r l−1∈[r−t,r−s]minsuml−1≤sumr
其中 min l − 1 ∈ [ r − t , r − s ] s u m l − 1 \min_{l-1 \in [r-t,r-s]} sum_{l-1} minl−1∈[r−t,r−s]suml−1 又是一个定长区间最值问题,单调队列优化后即可通过。
P3957 [NOIP2017 普及组] 跳房子 ★ I m p o r t a n t \tt\textcolor{Orange}{\bigstar Important} ★Important
我的题解:点击这里
显然,使 a j ∈ [ a i − d − g , a i − d + g ] a_j \in [a_i-d-g,a_i-d+g] aj∈[ai−d−g,ai−d+g] 成立的 j j j 一定是连续的,设它的范围是 [ l , r ] [l,r] [l,r]。当 i i i 不断增加,即 a i a_i ai 单调不减的时候, a i − d − g , a i − d + g a_i-d-g,a_i-d+g ai−d−g,ai−d+g 也单调不减, l , r l,r l,r 自然同样单调不减。
在一个 l , r l,r l,r 均单调不减的区间 [ l , r ] [l,r] [l,r] 上找最小值,可以用单调队列维护(参考例题:滑动窗口)。使单调队列内元素单调递减,每次 i i i 右移的时候,在其中加入新的 a j ≤ a i − d + g a_j \le a_i-d+g aj≤ai−d+g 的 j j j(可能不止一个);然后弹出 a j < a i − d − g a_j < a_i-d-g aj<ai−d−g 的 j j j,最后队首元素即为所求的 max f j \max f_j maxfj。
上面两种操作顺序不能改变,因为可能出现新加入的 j j j 不合法的情况(满足 a j ≤ a i − d + g a_j \le a_i-d+g aj≤ai−d+g 却不满足 a j ≥ a i − d − g a_j \ge a_i-d-g aj≥ai−d−g),这样加入后无法立即弹出,非法的答案就进入到队列当中了。这也解答了这个帖子中提出的问题。
上面几道例题,可能让大家认为“定长区间最值问题”都可以用单调队列维护。然而,这只是单调队列的一种比较经典的用法,且必须要求区间连续移动。
实际上,如上面引用的题解内容所说:“在一个 l , r l,r l,r 均单调不减的区间 [ l , r ] [l,r] [l,r] 上找最小值,可以用单调队列维护”。“定长区间最值问题”可以用单调队列维护的本质原因不是区间长度固定,而是寻找最值的区间左右端点均单调不减。
比如上面那道题,区间长度可能随时变化,有时甚至会缩成空集,但是只要它左右端点单调不减,就可以用单调队列求最值。
P2254 [NOI2005] 瑰丽华尔兹
最后放一道单调队列模板练习题,相信手打并调完这道题,你会对单调队列的各种细节和不同的打法倒背如流的 XD。
推荐题解:点击这里
总结 | Summary ★ I m p o r t a n t \tt\textcolor{Orange}{\bigstar Important} ★Important
综上所述,单调队列代码简短而好写,能够解决的问题范围清晰,是一种很实用的数据结构。单调队列不常作为一个裸的知识点来单独考,而是常常与动态规划等问题结合在一起,用作优化时间复杂度。
单调队列可以优化的问题具有以下特点:
- 在一个区间 [ l , r ] [l,r] [l,r] 上求最值;
- 区间左右端点 l , r l,r l,r 均单调不减/单调不增。
同时,类似滑动窗口这种 “(连续移动的)定长区间最值问题”是单调队列中考得最多的一种,也是必须掌握的一种。
下面附上一份更加通用的单调队列模板(或者说其实算是伪代码?):
定义/清空单调队列 //需要队头队尾均可压入和弹出,如双端队列
for(int i=1;i<=n;i++)
{
while(队列非空 && 队头超出范围) 弹出队头;
while(队列非空 && 队尾劣于当前元素i) 弹出队尾;
队尾压入i;
//此时队头为最优元素,按题目需要使用
}
当使用单调队列的不再是 OIer,而是规则的制订者,当数列里的不再是没有生命的数字,而是一个个活生生的人,无处不在的淘汰机制就是一个巨大的单调队列。人们总是钟爱年轻而实力强的,抛弃年长而实力弱的,而这往往也带来最高的效率。
那一个被
a
j
a_j
aj 所干掉的
a
i
a_i
ai、被新加入的 a[i]
一路碾死的 a[q[tail]]
们总是大多数人。不想成为他们中的一员,就只能不断提升自己。数列元素的命运在输入的那一刻已经注定,而人尚能不断提升,为生命争取不被淘汰的资格。
创作不易,如果对您有所帮助,还请点赞支持,谢谢!QwQ
本文采用「CC-BY-NC 4.0」创作共享协议,转载请注明作者及出处,禁止商业使用。