单调队列与滑动窗口
下面介绍单调队列的基本应用,了解如何通过单调队列获得优化。注意队列中“删除、去尾、窗口”的操作。我们以洛谷P1886为例子:
题目描述
有一个长为
n
n
n 的序列
a
a
a,以及一个大小为
k
k
k 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。
例如:
The array is
[
1
,
3
,
−
1
,
−
3
,
5
,
3
,
6
,
7
]
[1,3,-1,-3,5,3,6,7]
[1,3,−1,−3,5,3,6,7], and
k
=
3
k = 3
k=3。
输入格式
输入一共有两行,第一行有两个正整数
n
,
k
n,k
n,k。
第二行
n
n
n 个整数,表示序列
a
a
a
输出格式
输出共两行,第一行为每次窗口滑动的最小值
第二行为每次窗口滑动的最大值
样例 #1
样例输入 #1
8 3
1 3 -1 -3 5 3 6 7
样例输出 #1
-1 -3 -3 -3 3 3
3 3 5 5 6 7
提示
【数据范围】
对于
50
%
50\%
50% 的数据,
1
≤
n
≤
1
0
5
1 \le n \le 10^5
1≤n≤105;
对于
100
%
100\%
100% 的数据,
1
≤
k
≤
n
≤
1
0
6
1\le k \le n \le 10^6
1≤k≤n≤106,
a
i
∈
[
−
2
31
,
2
31
)
a_i \in [-2^{31},2^{31})
ai∈[−231,231)。
本题用暴力法很容易编程:从头到尾扫描,每次检查k个数,一共检查O(nk)次。暴力法显然会超时。下面用单调队列求解,它的时间复杂度为O(n)。
在本题中,单调队列有以下特征:
- 队头的元素始终是队列中最小的。根据需要输出队头,但是不一定弹出。
- 元素只能从队尾进入队列,从队头、队尾都可以弹出。
- 序列中每个元素都必须进入队列。
上面的题解有点抽象,我们以食堂排队打饭为例说明它。
大家到食堂排队打饭时都有一个心理,在打饭之前先看看有什么菜,如果不好吃就走了。不过,能不能看到和身高有关,站在队尾的人如果个子高,眼光就能越过前面的人看到里面的菜;如果个子矮,会被挡住看不见。
一个矮个子来排队,他希望队伍前面的人都比他更矮。如果他会魔法,他来排队时,队尾比他高的人就自动从队尾离开,新的队尾如果仍比他高,也会离开;最后新来的矮个子成了新的队尾,而且是最高的。他终于能看到菜了,让人兴奋的是,菜很好吃,所以他肯定不想走。
假设每个新来的人的魔法本领都比队列中的人更高,这个队伍就会变成这样:每个新来的人都能排到队尾,但是都会被后面来的高个子赶走。这样一来,这个队列就会始终满足单调性:从队头到队尾,由矮到高。
但是,让这个魔法纳闷的是,打饭阿姨一直忙自己的,顾不上打饭。所以排头的人等了一会儿,就走了,等待时间就是k。这里有一个附带现象:队伍的长度不会超过k。
那么输出是什么呢?每次新来一个排队的人,排头如果还没走,他就向阿姨喊一声,这就是输出最小值min。
以上是本题的实现模型,其本质是贪心思想。
下面举例描述算法流程,队列为{1,3,-1,-3,5,3,6,7},我们可以理解为身高。以输出最小值为例,下面表格中“输出队首”是本题的结果:
元素进入队尾 | 元素进队顺序 | 队列 | 窗口范围 | 队首是否在窗口内 | 输出队首 | 弹出队尾 | 弹出队首 |
---|---|---|---|---|---|---|---|
1 | 1 | {1} | [ 1 ] | 是 | |||
3 | 2 | {1,3} | [ 1 2 ] | 是 | |||
-1 | 3 | {-1} | [ 1 2 3 ] | 是 | -1 | 3,1 | |
-3 | 4 | {-3} | [ 2 3 4 ] | 是 | -3 | -1 | |
5 | 5 | {-3,5} | [ 3 4 5 ] | 是 | -3 | ||
3 | 6 | {-3,3} | [ 4 5 6 ] | 是 | -3 | 5 | |
6 | 7 | {3,6} | [ 5 6 7 ] | -3否,3是 | 3 | -3 | |
7 | 8 | {3,6,7} | [ 6 7 8 ] | 是 | 3 |
单调队列的时间复杂度:每个元素最多入队一次、出队一次,且出入队时间都是O(1),因此总时间复杂度为O(n)。因为题中需要逐一处理所有n个数,所以O(n)已经是能达到的最优复杂度。
从上面的过程中可以看出,单调队列有两个重要的操作:删头、去尾:
- 删头:如果队头的元素脱离了窗口,这个元素就没有用了,弹出它
- 去尾:如果新元素入队时,原队列的存在破坏了队列的单调性,就弹出它
以下给出具体的代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1000005;
int a[N];
deque<int> q;//队列中的数据实际上是元素在原序列中a[n]的位置
int main()
{
int n,m;
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
}
for(int i = 1; i <= n; i++)
{
while(!q.empty() && a[q.back()] > a[i])
{//新来的元素比队尾元素小
q.pop_back();//去尾
}
q.push_back(i);//元素下标进入队尾
if(i >= m)
{//每个窗口输出一次
while(!q.empty() && q.front() <= i - m)
{//注意这里q.front() <= i - m 是判断该元素是否应该在窗口内
q.pop_front();//删头
}
cout << a[q.front()] << " ";
}
}
cout << endl;
while(!q.empty())
{//清空队列,下面进行找最大值
q.pop_front();
}
for(int i = 1; i <= n; i++)
{
while(!q.empty() && a[q.back()] < a[i])
{
q.pop_back();//去尾
}
q.push_back(i);
if(i >= m)
{
while(!q.empty() && q.front() <= i - m)
{
q.pop_front();//删头
}
cout << a[q.front()] << " ";
}
}
cout << endl;
return 0;
}
测试结果如下:
提交: