题意
ZJM 有一个长度为 n 的数列和一个大小为 k 的窗口, 窗口可以在数列上来回移动. 现在 ZJM 想知道在窗口从左往右滑的时候,每次窗口内数的最大值和最小值分别是多少. 例如: 数列是 [1 3 -1 -3 5 3 6 7], 其中 k 等于 3.
Input
输入有两行。第一行两个整数n和k分别表示数列的长度和滑动窗口的大小,1<=k<=n<=1000000。第二行有n个整数表示ZJM的数列。
Output
输出有两行。第一行输出滑动窗口在从左到右的每个位置时,滑动窗口中的最小值。第二行是最大值。
输入样例
8 3
1 3 -1 -3 5 3 6 7
输出样例
-1 -3 -3 -3 3 3
3 3 5 5 6 7
提示
分析
这道题需要用到单调队列来解决。
- 单调队列
单调队列即为自队首到队尾满足单调性的队列结构,即队内元素出队顺序是满足单调性的。
为了满足队内元素的单调性,队列可以通过从队尾弹出元素来实现。
🌰——若自队首到队尾满足单调递增,当前待入队元素小于(等于)队尾元素,则将队尾元素循环弹出,直到入队元素大于队尾元素或队空,将该元素入队。
由于队列在两端都可以出队,因此它很适合维护局部的单调性,找到一个序列的一段区间内的最值。
- 单调队列与单调栈
看到单调队列就会想到单调栈👉[week5]最大矩形——单调栈(线性结构的应用)这两者存在相似,但也有不同。
-
相似处
两者都需要维持结构内的单调性,并通过压入和弹出来维持。 -
不同处
- 单调队列可以通过队尾和队首来双向维护单调性,但单调栈只能通过栈顶来维护。
- 单调栈通常用来在一个完整序列中维护单调性(全局),而单调队列通常用在一个完整序列中的部分区间维护单调性(局部)。
- 由于第2点,因此单调栈通常不会限制栈内元素个数,而单调队列通常会限制队列内元素的个数。
- 代码实现
滑动窗口即为一个移动的长度固定的区间,寻找一个局部区间内的最值,需要使用单调队列。
从区间的最左端开始,将区间的长度固定为题目所给要求k。将区间内的元素依次压入不同的单调队列,最后留下的队首元素即为该区间内的最大值或最小值。之后前移一格,进入下一个区间,将不在当前区间内的元素从队首弹出队列,将新的元素压入队列,留下的队首元素即为该区间的最值。重复上述操作,直到包含最后一个元素长度为k的区间扫描完。
【小tip:为什么可以直接通过队首弹出不属于当前区间的元素?
根据队列的压入顺序,可知越接近队首的元素其入队越早。如果队列中存在不属于当前窗口的元素,那么一定是从队首开始向后连续放置的元素。】
- 遇到的问题
这次的代码还是遇到了一些值得注意的问题。
1. 何时存储当前窗口的最值?
也就是说,如何设计代码使其自动在一个窗口的元素压入之后,将当前队列中的队首元素进行存储,并进入下一个窗口的检验。
一共有两种方法:
-
设置左指针,若当前扫描元素与左指针的距离为窗口大小+1,则存储队首元素,并前移左指针。
由于遍历完最后一个窗口后,整个遍历将结束,左指针不会再移动,如果不单独处理最后一个窗口,则会漏掉最后一个窗口的最值存储。因此当遍历到最后一个元素时,即代表遍历完最后一个窗口时,单独将此时的最值存储
-
单独处理第一个窗口,从第窗口大小+1个元素开始遍历。这意味着,在第一个窗口之后每一次的移动都是一个新窗口,因此每一次移动在排除队列中所有未在窗口内的元素后得到的队首元素即为一个新窗口的最值,不需要再判断何时存储。
比较两种方法的性能:
实际上这两种方法的性能没有太大差异。两种方法的遍历次数一致,实际上的压队、出队和存储操作次数都一样。我采用的是第一种,第一种方法多了n-k+1次判定,但实际上在vj上运行的时间甚至快于第二种方法(除了该部分处理其他一致时进行的测试)。
2. 超时问题:vector/scanf和printf
- vector
我非常喜欢用vector,因为我很害怕数组下标和容量的问题。因此我几乎在实验的所有代码中都会首选vector动态分配数组。在此之前也存在大范围的复杂数据,但没有出现过因为vector而超时的问题。
在这个代码中,分别存储最大值和最小值的两个数组都使用vector时就会出现超时。这个问题真的讨论和调试了很久很久才发现。
在这里分析一下原因吧。
💡vector模板分配空间的机制是在空间不足时,重新分配空间,再把以前的数据转存到新空间中,之后释放原空间。
也就是说,vecotr每次都需要重新分配空间和复制数据。当数据量过大、vector数组过多时,很有可能出现超时。
- 输入输出
只要在大数据题目的提交中出现超时,在关闭同步流之后仍然超时,建议一定要尝试将输入输出都改为c模式!
总结
- 因为超时较劲到凌晨四点,结果还是第二天起来和朋友讨论和测试了两个小时才发现的👋这真的给了我一个教训,不要那么依赖vector☝️
- 输入输出真是杀我🤐
代码
//
// main.cpp
// lab-d
//
//
#include <iostream>
#include <vector>
#include <deque>
#include <algorithm>
using namespace std;
//vector<int> number;
int number[10000000];
deque<int> Max;
deque<int> Min;
//deque<int> order1;
//deque<int> order2;
//vector<int> ans1;
//vector<int> ans2;
int ans1[10000000];
int ans2[10000000];
//int Min[1000000];
//int Max[1000000];
//int top1 = 0,top2 = 0,back1 = 0,back2 = 0,order1 = 0,order2 = 0;
/*
int main()
{
ios::sync_with_stdio(false);
int n = 0,k = 0,left = 0,j = 0;
// cin>>n>>k;
scanf("%d %d",&n,&k);
for( int i = 0 ; i < n ; i++ )
{
// cin>>a;
// number.push_back(a);
scanf("%d",&number[i]);
}
// pair<int, int> max(number[0],0);
//处理第一个窗口,使得之后从第k+1个开始每移动一个就是一个新窗口
for( int i = 0 ; i < k ; i++ )
{
while( !Min.empty() && number[i] < number[Min.back()] )
Min.pop_back();
Min.push_back(i);
while( !Max.empty() && number[i] > number[Max.back()] )
Max.pop_back();
Max.push_back(i);
}
// ans1.push_back(number[Min.front()]);
// ans2.push_back(number[Max.front()]);
ans1[j] = number[Min.front()];
ans2[j] = number[Max.front()];
j++;
//由于从第k+1个开始,因此每遍历一个都是一个新窗口,此时得到的就为当前新窗口的最值
//在排除不在窗口内的最值之后,剩下的最值直接为当前窗口的最值
for( int i = k ; i < n ; i++ )
{
// cout<<i<<"------"<<endl;
while( !Min.empty() && number[i] <= number[Min.back()] )
Min.pop_back();
Min.push_back(i);
while( !Min.empty() && i - Min.front() + 1 > k )
Min.pop_front();
while( !Max.empty() && number[i] >= number[Max.back()] )
Max.pop_back();
Max.push_back(i);
while( !Max.empty() && i - Max.front() + 1 > k )
Max.pop_front();
ans1[j] = number[Min.front()];
ans2[j] = number[Max.front()];
j++;
}
for( int i = 0 ; i < j ; i++ )
printf("%d ",ans1[i]);
cout<<endl;
for( int i = 0 ; i < j ; i++ )
printf("%d ",ans2[i]);
return 0;
}*/
int main()
{
ios::sync_with_stdio(false);
int n = 0,k = 0,left = 0,j = 0;
// cin>>n>>k;
scanf("%d %d",&n,&k);
for( int i = 0 ; i < n ; i++ ) //输入所有数
{
// cin>>a;
// number.push_back(a);
scanf("%d",&number[i]);
}
// pair<int, int> max(number[0],0);
for( int i = 0 ; i < n ; i++ )
{
// cout<<i<<"------"<<endl;
if( i - left == k ) //每k个记录一次最值(通过左指针与当前遍历数的距离判断
{
// ans1.push_back(number[Min.front()]);
// ans2.push_back(number[Max.front()]);
ans1[j] = number[Min.front()];
ans2[j] = number[Max.front()];
j++;
left++; //左指针前移
}
while( !Min.empty() && i - Min.front() + 1 > k ) //移除不在窗口内的最小值
Min.pop_front();
while( !Min.empty() && number[i] < number[Min.back()] ) //将当前数放入队列中
Min.pop_back();
Min.push_back(i);
while( !Max.empty() && i - Max.front() + 1 > k ) //移除不在窗口内的最小值
Max.pop_front();
while( !Max.empty() && number[i] > number[Max.back()] ) //将当前数放入队列中
Max.pop_back();
Max.push_back(i);
if( i == n-1 ) //当遍历到最后一个数时,将最后一个窗口的最值存储
{
ans1[j] = number[Min.front()];
ans2[j] = number[Max.front()];
j++;
}
}
for( int i = 0 ; i < j ; i++ ) //输出
printf("%d ",ans1[i]);
cout<<endl;
for( int i = 0 ; i < j ; i++ )
printf("%d ",ans2[i]);
return 0;
}