1、概念
动窗口(Sliding Window)是一种常用的算法技巧,主要用于处理数组或列表中的连续子序列问题。它通过维护一个大小固定或可变的"窗口"来高效地解决问题,避免了不必要的重复计算。
2、实战例子
给定一个数组,给定滑动窗口k,从左到右,求出每个窗口中最小值和最大值。
思路:通过一维数组存储数据;通过双端队列存储滑动窗口中的最值。
#include <iostream>
#include <deque>
#include <algorithm>
using namespace std;
const int N = 1e6 + 10; // 定义数组最大大小
int a[N]; // 存储输入数据的数组
int main()
{
int n, k; // n-数组长度,k-滑动窗口大小
cin >> n >> k; // 输入数组长度和窗口大小
for (int i = 1; i <= n; i++) cin >> a[i];
deque<int> q; // 使用双端队列维护滑动窗口
// 第一部分:求滑动窗口最小值
for(int i = 1; i <= n; i++)
{
// 维护单调递增队列:移除队尾大于当前元素的元素
while(q.size() && q.back() > a[i]) q.pop_back();
q.push_back(a[i]); // 将当前元素加入队尾
// 如果队首元素是窗口左侧刚移出的元素,则移除它
if(i - k >= 1 && q.front() == a[i - k]) q.pop_front();
// 当窗口完全在数组内时,输出当前窗口最小值
if(i >= k) cout << q.front() << ' ';
}
q.clear(); // 清空队列,准备下一轮计算
cout << endl; // 输出换行
// 第二部分:求滑动窗口最大值
for(int i = 1; i <= n; i++)
{
// 维护单调递减队列:移除队尾小于当前元素的元素
while(q.size() && q.back() < a[i]) q.pop_back();
q.push_back(a[i]); // 将当前元素加入队尾
// 如果队首元素是窗口左侧刚移出的元素,则移除它
if(i - k >= 1 && q.front() == a[i - k]) q.pop_front();
// 当窗口完全在数组内时,输出当前窗口最大值
if(i >= k) cout << q.front() << ' ';
} return 0;
}
难点分析:
1、while(q.size() && q.back() > a[i]) q.pop_back();
求最小值,维护的是递增的双端队列,头部是最小值;在进行新插入元素比较时,首先队列中需要有元素,其次就是比较插入元素和栈顶元素;
2、if(i - k >= 1 && q.front() == a[i - k]) q.pop_front();
当i == k的时候,说明刚好形成了第一个窗口;这个时候是不需要从队列中移除任何数据的,直接输出队列中的最小值即可;
当i > k的时候,说明滑动窗口已经开始移动了,那么在数组的视角,只要滑动窗口向前移动一步,那么一定有一个新数据加到滑动窗口,同时有一个旧的数据脱离滑动窗口;因此:当移动一步的时候,我们输出的最小值一定不能是刚脱离滑动窗口的这个数;
而每次窗口的位置是[i - k + 1, i],所以a[i - k]位置的数一定不能作为当前窗口的输出!
q.front() == a[i - k],这里的a[i-k]是从a[1]开始判断的,当i = 4的时候,a[1]不能作为输出;此时队列的最小值可以是a[2]或a[3]或a[4],但就是不能是a[1]。而且,a[1]是第一个进行加入队列操作的,它要么在队列第一个元素,要么被比较而移除出去了,一定不会出现在队列的其他位置;所以通过这个式子就可以彻彻底底确定当前窗口的输出不会是a[1]。后续就是类似了。。。