题目概述
解题思路
这题要想暴力求解,其实就是每个滑窗求个最值,这样时间复杂度就是O(N * log(W))。这题看着应该有复杂度是O(N)的解法。
想想看该怎么做呢?通常的做法是:找到一个更贴近题目的规律 + 空间换时间的思路。
- 首先思考一下,如何用空间换时间。一般来说,这种带有窗口的题目,维护一个窗口大小的数组就差不多了。这里我们不妨维护一个窗口大小的最大值数组。
- 然后就是找规律。我们维护的数组显然与当前滑窗附近的这几个滑窗的最大值有关,那么随着滑窗的右移,最值的变化有怎样的规律呢?
- 如果遇到的数字比现有的最大值更大,那么之前的最值就没有价值了(因为之后几个滑窗的最值不会小于这个值)
- 如果遇到的数字比现有的最大值小,那就要考虑保留它,因为很可能当前的最大值马上就失效了。
从这个思路出发,我们维护一个存储窗口最大值的数组(队列)q_max。
- 首先,这个队列的尺寸不得超过窗口的大小,这样就能保证最大值的时效性。
- 然后,q_max的队首元素与当前滑窗的最大值相关,这样便于获取当前滑窗的最大值。
- 接着是更新队列的策略:
- 如果碰到的数组中新的元素小于当前队尾的元素,则将该元素放入队列;
- 如果该元素大于队尾的元素,则将队尾的元素丢弃,直至该元素大于队尾的元素或者队列中无元素。
- 考虑到我们需要确定队首元素的时效性,也就是比较队首元素与当前滑窗和窗口宽度的位置关系,因此不妨在队列里存储元素的序号。
小知识:这里我们采用了STL的双端队列deque (double ended queue的缩写,头文件是deque.h),这个数据结构的特点是便于在头部和尾部插入元素。
本题用到的数据结构类型也被称作“单调队列”,它能动态地维护定长序列中的最值,可应用于求最值与优化DP转移等方面。
原理
单调队列实质上是一类双端队列(deque),在加入一个元素时,我们通过一定操作维护队列的单调性。该队列特征的操作便是“后移一位”。顾名思义,后移一位就是指队列维护的区间往后移一位。这个操作中要做的事情是弹出队首与加入队尾。
加入队尾
为了保证这个队列中元素的单调性,对于加入的元素,我们可能要删除一些队尾的元素。以不下降的单调队列为例(下同),插入时要弹出所有比要插入元素大的队尾元素,直到队尾元素不再比其大再插入。此时插入元素时要记下插入元素在原序列中的位置,这个信息将会对弹出队首时发挥作用。
弹出队首
对于插入的元素,我们检查它与队首元素的位置之差,当这个差大于队列定长时说明队首元素已在区间之外,应当弹出。
算法性能
这个方法的空间复杂度是O(N+W);
时间复杂度是滑窗的次数O(N) + 元素进出队列的次数O(N * 2),所以也是O(N)。
示例代码
#include<iostream>
#include<algorithm>
#include<string>
#include<vector>
#include<set>
#include<map>
#include<stack>
#include<cstring>
#include<queue>
#include<deque>
#include<iomanip>
using namespace std;
int main()
{
int list_len, win_len, temp_max = 0;
int data[1000001];
deque<int> qmax;
cin >> list_len >> win_len;
for (int di = 0; di < list_len; ++di)
scanf("%d", data + di);
for (int di = 0; di < list_len; ++di)
{
if (qmax.empty())
qmax.push_back(di);
else
{
if (data[di] <= data[qmax.back()])
qmax.push_back(di);
else
{
while (!qmax.empty() && (data[di] > data[qmax.back()]))
qmax.pop_back();
qmax.push_back(di);
}
}
temp_max = qmax.front();
if ((di - win_len) >= temp_max)
qmax.pop_front();
if (di + 1 >= win_len)
cout << data[qmax.front()] << " ";
}
return 0;
}