洛谷P1886 滑动窗口【模板】单调队列详解
题目描述
给定一个长度为n的整数序列,要求输出所有长度为k的连续子数组的:
- 最小值(第一部分输出)
- 最大值(第二部分输出)
数据范围:
- 1 ≤ k ≤ n ≤ 10^6
- 时间限制:1s
- 空间限制:128MB
算法思路
本题是单调队列模板题,核心思想是通过维护一个双端队列来高效获取滑动窗口的极值。
暴力法缺陷
直接对每个窗口遍历求极值的时间复杂度为O(nk),当n=1e6时会超时。需要优化到O(n)时间复杂度。
单调队列原理
维护一个双向队列,保证队列元素按单调顺序排列:
- 最小值队列:保持升序(队头到队尾递增)
- 最大值队列:保持降序(队头到队尾递减)
当新元素入队时:
- 移除所有比当前元素大的元素(最小值队列)或小的元素(最大值队列)
- 将当前元素加入队尾
- 移除所有超出窗口范围的队头元素
代码解析
cpp
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e6 + 10;
int n, k;
int a[MAXN]; // 原始数组
int que[MAXN]; // 单调队列
int ffront, bback; // 队列头尾指针
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n >> k;
for(int i = 1; i <= n; ++i) cin >> a[i];
// 处理最小值部分
ffront = 1, bback = 0;
for(int i = 1; i <= n; ++i) {
// 移除超出窗口的队头元素
while(ffront <= bback && que[ffront] < i - k + 1) ffront++;
// 维护单调性:移除所有>=当前值的队尾元素
while(ffront <= bback && a[que[bback]] >= a[i]) bback--;
que[++bback] = i;
// 窗口形成后输出结果
if(i >= k) cout << a[que[ffront]] << " ";
}
cout << "\n";
// 处理最大值部分
ffront = 1, bback = 0;
for(int i = 1; i <= n; ++i) {
while(ffront <= bback && que[ffront] < i - k + 1) ffront++;
// 维护单调性:移除所有<=当前值的队尾元素
while(ffront <= bback && a[que[bback]] <= a[i]) bback--;
que[++bback] = i;
if(i >= k) cout << a[que[ffront]] << " ";
}
return 0;
}
关键点说明
-
队列初始化:
- 使用数组模拟双端队列,
ffront
指向队头,bback
指向队尾 - 初始时队列为空(
ffront > bback
)
- 使用数组模拟双端队列,
-
窗口维护:
- 窗口有效性检查 :
que[ffront] < i - k + 1
判断队头是否已出窗口 - 单调性维护 :通过
while
循环移除不符合单调性的队尾元素
- 窗口有效性检查 :
-
时间复杂度:
- 每个元素最多入队出队各一次,总时间复杂度O(n)
算法对比
方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
---|---|---|---|
暴力法 | O(nk) | O(1) | n≤1e4 |
优先队列 | O(nlogk) | O(k) | 需要动态极值但可接受log |
单调队列 | O(n) | O(k) | 滑动窗口极值问题 |
注意事项
- 数组越界:使用1-based索引避免边界判断错误
- 队列为空:本题保证k≤n,无需额外处理空队列情况
- 输出格式:注意两部分输出间的换行符
扩展应用
单调队列思想可应用于:
- 动态规划优化(如DP滑动窗口技巧)
- 实时数据流处理
- 股票买卖问题(求某段时间内的最大利润)
总结
本题通过单调队列将滑动窗口极值问题的时间复杂度优化到线性级别。核心在于理解:
- 如何维护队列的单调性
- 如何高效移除过期元素
- 如何处理窗口滑动时的边界情况
掌握此模板可解决LeetCode 239、剑指Offer 59等经典题目。