单调队列笔记

单调队列笔记

双端队列deque维护一个严格单调变化的组,可以称为一个单调队列

单调队列因为可以直接对组的两端进行操作,所以可以有效的降低时间复杂度

用单调队列来解决问题,一般是需要得到的某个范围内的最小值或最大值

这里以一道经典的单调队列的题目为例:


题目描述

有一个长为 \(n\) 的序列 \(a\),以及一个大小为 \(k\) 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。

例如,对于序列 \([1,3,-1,-3,5,3,6,7]\) 以及 \(k = 3\),有如下过程:

\[\def\arraystretch{1.2} \begin{array}{|c|c|c|}\hline \textsf{窗口位置} & \textsf{最小值} & \textsf{最大值} \\ \hline \verb![1 3 -1] -3 5 3 6 7 ! & -1 & 3 \\ \hline \verb! 1 [3 -1 -3] 5 3 6 7 ! & -3 & 3 \\ \hline \verb! 1 3 [-1 -3 5] 3 6 7 ! & -3 & 5 \\ \hline \verb! 1 3 -1 [-3 5 3] 6 7 ! & -3 & 5 \\ \hline \verb! 1 3 -1 -3 [5 3 6] 7 ! & 3 & 6 \\ \hline \verb! 1 3 -1 -3 5 [3 6 7]! & 3 & 7 \\ \hline \end{array} \]

输入格式

输入一共有两行,第一行有两个正整数 \(n,k\)
第二行 \(n\) 个整数,表示序列 \(a\)

输出格式

输出共两行,第一行为每次窗口滑动的最小值
第二行为每次窗口滑动的最大值

样例输入

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

样例输出

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

数据范围
对于 \(50\%\) 的数据,\(1 \le n \le 10^5\)
对于 \(100\%\) 的数据,\(1\le k \le n \le 10^6\)\(a_i \in [-2^{31},2^{31})\)


题目所给的窗口 ,我们可以使用一个单调队列来维护,每次移动时,检查新进窗口的元素,和退出窗口的元素

然后输出单调队列的最小值或最大值

可以将朴素做法(使用两次循环)的 \(O(n*k)\) 的时间复杂度优化到 \(O(n+k)\),得到极大的改进,减少了重复的比较次数


首先的是单调队列的写法,可以采用数组模拟队列的方法或者STL库内的deque

这里先介绍数组模拟的方式:

  • 定义数组队列、队列的头、队列的尾
int que[1000010], hh, tt;
  • 然后将头和尾元素进行初始化:
int hh = 0, tt = -1;

hh一开始指向的是a[]的第一个元素的下标 \(0\) ,此时队列中的元素个数为 \(0\),故tt=-1

注意!!!这里的单调队列存储的是a[]数组的下标

先来输出窗口内的最小值:
  • 遍历a[]数组
for (int i = 0; i < n; i++) {
......
}
  • 其中,我们需要进行的是队列的维护
if (hh <= tt && i - k + 1 > q[hh]) hh++;

hh<=tt即队列不为空时,若队列中头元素存储的下标值不在a[]数组的窗口内,那么就将头元素向后移动一位(在严格单调队列中),(相当于弹出头元素)

while (hh <= tt && a[q[tt]] >= a[i]) tt--;
q[++tt] = i;

hh<=tt即队列不为空时,若新进窗口的元素小于我们单调队列的尾元素存储的下标对应的值,就将尾元素向前移动(相当于弹出队尾元素),直到插入a[i]的下标后形成一个严格单调队列

a[i]的下标作为尾元素,存进队列中

if (i >= k - 1) printf("%d ", a[q[hh]]);

判断窗口是否完整,输出窗口中的最小值

同理,可以依葫芦画瓢地写出输出窗口中最大值的代码:
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]]);
}

下面是使用deque实现的代码:

deque<int> q;

定义队列 q,同样的思想可以得到如下代码:

for (int i = 0; i < n; i++) {
while (!q.empty() && i - k + 1 > q.front()) q.pop_front();
while (!q.empty() && a[i] <= a[q.back()]) q.pop_back();
q.push_back(i);
if (i - k + 1 >= 0) printf("%d ", a[q.front()]);
}

q.empty()判断队列是否为空,q.front()返回队头元素、q.back()返回队尾元素

q.pop_front()弹出队头元素、q.pop_back()弹出队尾元素

最后使用q.clear()清空队列,同理得到剩余部分的代码

核心AC代码如下:

模拟队列:

int n, k;
int a[1000010], q[1000010];
int main()
{
scanf("%d %d", &n, &k);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
int 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]]);
}
printf("\n");
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]]);
}
return 0;
}

使用STL库:

int n, k;
int a[1000010];
deque<int> q;
int main()
{
scanf("%d %d", &n, &k);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
for (int i = 0; i < n; i++) {
while (!q.empty() && i - k + 1 > q.front()) q.pop_front();
while (!q.empty() && a[i] <= a[q.back()]) q.pop_back();
q.push_back(i);
if (i - k + 1 >= 0) printf("%d ", a[q.front()]);
}
printf("\n");
q.clear();
for (int i = 0; i < n; i++) {
while (!q.empty() && i - k + 1 > q.front()) q.pop_front();
while (!q.empty() && a[i] >= a[q.back()]) q.pop_back();
q.push_back(i);
if (i - k + 1 >= 0) printf("%d ", a[q.front()]);
}
return 0;
}
posted @   才瓯  阅读(20)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示