单调队列-滑动窗口算法一篇学会-AcWing 154. 滑动窗口
滑动窗口题解
题目传送门:
AcWing 154. 滑动窗口
题目描述
给定一个大小为 n ≤ 10^6 的数组和一个大小为 k 的滑动窗口,窗口从数组最左端移动到最右端。要求输出窗口在每个位置时的最小值和最大值。
样例解释:
输入数组为 [1 3 -1 -3 5 3 6 7],k=3
输出:
最小值序列:-1 -3 -3 -3 3 3
最大值序列:3 3 5 5 6 7
题目分析
这是一个典型的滑动窗口问题,需要在O(n)时间复杂度内解决。直接暴力求解对于每个窗口遍历k个元素的话,时间复杂度会是O(nk),对于n=1e6来说不可接受。
解题思路
使用单调队列来优化:
- 最小值:维护一个单调递增队列,队首元素即为当前窗口最小值
- 最大值:维护一个单调递减队列,队首元素即为当前窗口最大值
算法讲解
- 队列中存储的是数组元素的下标而非值本身,这样可以方便判断元素是否在窗口内
- 每次移动窗口时:
- 先检查队首元素是否还在窗口内,不在则弹出
- 然后从队尾开始,删除所有不满足单调性的元素
- 最后将当前元素加入队列
- 当窗口形成后(i≥k时),输出队首元素
代码实现
#include <bits/stdc++.h>
using namespace std;
// #define int long long
const int N = 1e6 + 10;
int n, k;
int a[N];
int q[N]; // 队列存的是下标
void solve()
{
cin >> n >> k;
for (int i = 1; i <= n; i++)
cin >> a[i];
// 找每个窗口的最小值
int tt = -1, hh = 0; // 初始化队列
for (int i = 1; i <= n; i++)
{
// 维护队列中的元素在窗口范围内
while (hh <= tt && i - k + 1 > q[hh]) // 队首元素不在窗口内
hh++;
// 维护单调递增队列
while (hh <= tt && a[q[tt]] >= a[i]) // 队尾元素大于等于当前元素
tt--;
q[++tt] = i; // 将当前元素下标加入队列
if (i >= k) // 窗口形成后才输出
cout << a[q[hh]] << " ";
}
cout << "\n";
// 找每个窗口的最大值
tt = -1, hh = 0; // 重置队列
for (int i = 1; i <= n; i++)
{
// 维护队列中的元素在窗口范围内
while (hh <= tt && i - k + 1 > q[hh])
hh++;
// 维护单调递减队列
while (hh <= tt && a[q[tt]] <= a[i])
tt--;
q[++tt] = i;
if (i >= k)
cout << a[q[hh]] << " ";
}
}
int main()
{
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
solve();
return 0;
}
重点细节
- 队列存储的是下标:这样可以方便判断元素是否还在窗口内
- 双条件while循环:
- 第一个while保证队首元素在窗口内
- 第二个while维护队列的单调性
- 窗口形成条件:只有当i≥k时才输出结果
- 队列初始化:每次处理前需要重置队列指针
复杂度分析
- 时间复杂度:O(n),每个元素最多入队和出队一次
- 空间复杂度:O(n),用于存储队列
总结
单调队列是解决滑动窗口极值问题的高效方法,关键在于:
- 维护队列的单调性
- 及时移除不在窗口内的元素
- 理解队列中存储下标而非值的好处
本题的经典解法值得熟练掌握,类似的滑动窗口问题都可以考虑使用单调队列优化。