原题链接: AcWing 154.滑动窗口
关键词: 单调队列
给定一个大小为 n≤106 的数组。
有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k 个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为 [1 3 -1 -3 5 3 6 7]
,k 为 3。
窗口位置 | 最小值 | 最大值 |
---|---|---|
1 3 -1 -3 5 3 6 7 | -1 | 3 |
1 3 -1 -3 5 3 6 7 | -3 | 3 |
1 3 -1 -3 5 3 6 7 | -3 | 5 |
1 3 -1 -3 5 3 6 7 | -3 | 5 |
1 3 -1 -3 5 3 67 | 3 | 6 |
1 3 -1 -3 5 3 6 7 | 3 | 7 |
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
输入格式
输入包含两行。
第一行包含两个整数 n 和 k,分别代表数组长度和滑动窗口的长度。
第二行有 n 个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
输入样例:
8 3
1 3 -1 -3 5 3 6 7
输出样例:
-1 -3 -3 -3 3 3
3 3 5 5 6 7
思路:
如果使用普通队列:
- 从下标0到n-1每次往后移动一位,用普通队列维护长度为k的一个窗口时间复杂度为O(n)
- 输入时候扫描整个队列,输出最大值和最小值(时间复杂度为O(k))
用普通队列的方式总时间复杂度为O(nk),本题n<106,一定会超时,得优化为O(n)的算法
使用单调队列:
单调队列的最大/小值就在对头/尾,因此能够以 O(1) 的效率获得最大值或者最小值,因此总体时间复杂度为 O(n)
- 求最小值时,维护一个单调递增的单调队列,如果当前数小于等于队尾元素,就将队尾删掉,然后再将当前数入队
- 最大值维护一个单调递减的单调队列
注意: 最后从下标k - 1开始往后,才输入结果
代码实现:
#include <iostream>
using namespace std;
const int maxn = 1000010;
int a[maxn], q[maxn], n, k; //a存数据 q队列(存下标) n个数 k窗口长度
int main(){
cin >> n >> k;
for(int i = 0; i < n; i ++ ) cin >> a[i];
int hh = 0, tt = -1; //hh队头 tt队尾 (队头在左边 队尾在右边)
for(int i = 0; i < n; i ++ ){
// 队列非空且对头不在窗口内时,将队头加入窗口
if(hh <= tt && q[hh] < i - k + 1) hh++;
// 队列非空且当前元素小于队尾元素 删掉队尾
while(hh <= tt && a[i] <= a[q[tt]]) tt--;
// 当前元素加入队列
q[++tt] = i;
if(i >= k - 1) cout << a[q[hh]] << ' ';
}
// 换行
puts("");
hh = 0, tt = -1;
for(int i = 0; i < n; i ++ ){
if(hh <= tt && q[hh] < i - k + 1) hh++;
// 找最大值就维护一个单调递减序列 队列头为最大值
while(hh <= tt && a[i] >= a[q[tt]]) tt--;
q[++tt] = i;
if(i >= k - 1) cout << a[q[hh]] << ' ';
}
return 0;
}