AcWing.154 滑动窗口
1.题目
1.1内容描述
给定一个大小为 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 6] 7 | 3 | 6 |
1 3 -1 -3 5 [3 6 7] | 3 | 7 |
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
1.2 时空限制:1s/64MB
1.3 输入格式
输入包含两行。
第一行包含两个整数 n 和 k,分别代表数组长度和滑动窗口的长度。
第二行有 n 个整数,代表数组的具体数值。
同行数据之间用空格隔开。
1.4 输出格式
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
输入样例
8 3
1 3 -1 -3 5 3 6 7
输出样例
-1 -3 -3 -3 3 3
3 3 5 5 6 7
2.问题分析
本题是一道经典的模板题。首先我们来看一下滑动窗口的定义,对样例中的数组(不妨记为A)进行遍历,当遍历到A[2]=-1时,由于窗口大小k = 3,于是滑动窗口形成了,即为A[0]~A[2]。
我们接着向后遍历元素,为了保持窗口大小k = 3,我们在加入A[3]的同时,势必要去掉A[0],即窗口向右滑动一位。这样一直遍历,直到最后一个元素也被遍历完。
以求解最小值为例。
(1)记滑动窗口内随机两个数组下标为i和j(规定i<j),那么当滑动窗口向右移动时,若A[i]仍在滑动窗口内,A[j]也在滑动窗口内。如果恰好A[i]>A[j],那A[i]就一定不是最小值了,我们可以将其排除在外。
(2)由此,我们可以使用单调队列来解决本题。单调队列是一种数据结构,除了队列的基本特点外,其特点是队列的队头到队尾具有严格单调性,且队尾也可以出队。
(3)当滑动窗口向右移动时,我们需要把一个新的元素加入到单调队列中。为了满足单调的性质,在加入新元素时需要与队尾元素比较,因为我们要求最小值,所以当队尾元素大于新元素时,我们就将队尾元素出队(即为(1)中将A[i]排除的过程)。重复此过程,直到队尾元素小于新元素。由此我们就可以保证队列具有严格的单调性(从队头到队尾严格单调递增)。于是,当前滑动窗口的最小值即为队头的值。
(4)考虑到滑动窗口的大小是被限制为k的,所以我们还要保证单调队列中的元素都是窗口中的,所以,在队头不在滑动窗口中时,队头出队。
以题目例子(求最小值)阐述过程。
样例数组:1 3 -1 -3 5 3 6 7
滑动窗口大小k = 3
deque<int> dequeA
(1) A[0]入队, ->dequeA = {1}
(2) A[1]入队, ->dequeA = {1,3}
(3) A[2]入队,由于A[2]<dequeA.back() = 3,于是队尾元素3出队,
再次比较,由于A[2]<dequeA.back() = 1,于是队尾元素1出队,
单调队列无元素,A[2]直接入队, ->dequeA = {-1}
(4) A[3]入队,由于A[3]<dequeA.back() = -1,于是队尾元素-1出队,->dequeA = {-3}
(5) A[4]入队, ->dequeA = {-3,5}
(6) A[5]入队,由于A[5]<dequeA.back() = 5,于是队尾元素5出队,
再次比较,A[5]>dequeA.back() = -3 ,直接入队, ->dequeA = {-3,3}
(7) A[6]入队,
检查发现,队头已经不在窗口中(当前窗口范围为A[4]~A[6],而A[3] = -3),于是队头出队。
->dequeA = {3,6}
(8) A[7]入队, ->dequeA = {3,6,7}
3.C++代码
3.1使用STL的双端队列:
#include<iostream>
#include <deque>
using namespace std;
const int maxn = 1000001;
int Array[maxn];
deque<int> q;
int main()
{
int N, K;
cin >> N >> K;
for (int i = 1; i <= N; i++)
cin >> Array[i];
//最小值
for(int i = 1;i<=N;i++)
{
//队尾元素大于新元素时,就将队尾元素出队
while (!q.empty() && q.back() > Array[i])
q.pop_back();
//如果队头不在滑动窗口中,队头出队
//因为没有记录队头在原来数组中的位置,
//只能用q.front() == Array[i - K]作为判断条件,
//导致单调队列不能使用严格单调,(本来q.back() >= Array[i])
//实际上判断条件应该是r<=i-K,其中r是q.front()在原来数组的位置
if (!q.empty() &&i - K >= 1 && q.front() == Array[i - K])
q.pop_front();
//将新元素放入队尾
q.push_back(Array[i]);
//当窗口满了之后,输出队头,即最小值
if (i>=K)
cout << q.front() << " ";
}
q.clear();
cout << endl;
//最大值同最小值
for (int i = 1; i <= N; i++)
{
while (!q.empty()&& q.back() < Array[i])
q.pop_back();
if (!q.empty() &&i - K >= 1 && q.front() == Array[i - K])
q.pop_front();
q.push_back(Array[i]);
if (i>=K)
cout << q.front() << " ";
}
q.clear();
cout << endl;
return 0;
}
运行时间:264ms
上述方法直接存储元素,不容易判断元素是否已经离开窗口,所以我们优化方法,使单调队列只存储数组的下标。于是有以下代码:
3.2下标优化
#include<iostream>
#include <deque>
using namespace std;
const int maxn = 1000001;
int Array[maxn];
deque<int> index;
int main()
{
int N, K;
cin >> N >> K;
for (int i = 1; i <= N; i++)
cin >> Array[i];
//最小值
for(int i = 1;i<=N;i++)
{
//队尾元素大于新元素时,就将队尾元素出队,这里是严格单调
while (!index.empty() && Array[index.back()] >= Array[i])
index.pop_back();
//如果队头不在滑动窗口中,队头出队
if (!index.empty() && index.front() <= i - K)
index.pop_front();
//将新元素放入队尾
index.push_back(i);
//当窗口满了之后,输出队头,即最小值
if (i>=K)
cout << Array[index.front()] << " ";
}
index.clear();
cout << endl;
//最大值同最小值
for(int i = 1;i<=N;i++)
{
while (!index.empty() && Array[index.back()] <= Array[i])
index.pop_back();
if (!index.empty() && index.front() <= i - K)
index.pop_front();
index.push_back(i);
if (i>=K)
cout << Array[index.front()] << " ";
}
index.clear();
cout << endl;
return 0;
}
运行时间:246ms
那么我们是否可以不使用STL容器呢,答案是肯定的,我们可以采用数组加指针的方式来模拟单调队列。
3.3数组模拟单调队列
#include<iostream>
using namespace std;
const int maxn = 10000001;
int queue[maxn],arr[maxn];
//这里queue数组存储的也是下标而非数本身
int main(){
int N,K;
cin>>N>>K;
for(int i = 1;i<=N;i++)
cin>>arr[i];
//设置头指针和尾指针
//我们规定head<tail时,单调队列无元素
int head = 1,tail = 0;
//最小值
for(int i = 1;i<=N;i++){
//队尾元素大于新元素时,就将队尾元素出队,这里是严格单调
while(tail>=head&&arr[queue[tail]]>=arr[i])
tail--;
//如果队头不在滑动窗口中,队头出队
if(tail>=head&&queue[head] <= i-K)
head++;
//这里一定是++tail,而不是tail++
queue[++tail] = i;
if(i>=K)
cout<<arr[queue[head]]<<" ";
}
cout<<endl;
head = 1,tail = 0; //指针复位
//最大值
for(int i = 1;i<=N;i++){
while(tail>=head&&arr[queue[tail]]<=arr[i])
tail--;
if(tail>=head&&queue[head] <= i-K)
head++;
queue[++tail] = i;
if(i>=K)
cout<<arr[queue[head]]<<" ";
}
cout<<endl;
return 0;
}
运行时间:204ms
对比时间我们也可以发现,存储下标比存储数字本身时间开销小,使用数组模拟比使用STL的时间开销小。