赞
踩
下面介绍单调队列的基本应用,了解如何通过单调队列获得优化。注意队列中“删除、去尾、窗口”的操作。我们以洛谷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
输出共两行,第一行为每次窗口滑动的最小值
第二行为每次窗口滑动的最大值
8 3
1 3 -1 -3 5 3 6 7
-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; }
测试结果如下:
提交:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。