链接:滑动窗口
题目详情:
给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。
分析:
单调队列能够解决这类问题,首先我们维护一个单调不减的队列,即队头为最大值,然后如果窗口大小足够K了,之后每次一个新的数据入队就相当于把整个窗口往右滑动一格,由于窗口有大小,当队头的小标和当前入队的下标的偏移量大于K,说明窗口大小溢出了,这时直观的操作一定是剔出队头的冗余,为什么呢?因为队头的元素在之前一定用过了,此时的窗口已经得不到那个位置的贡献了。做完这个操作后我们要访问这个窗口的答案,即为队头。这个问题也可以改成某个数及左边的K-1个数最大值问题。做法一致都是窗口问题。
注意和单调栈的区别,单调栈是在pop掉一个元素时,此时才去考虑它的答案问题,经典问题就是那个倒水问题(求某一元素左(右)边第一个比它大(小)的值),注意大小的方向
Code:
//法一
class Solution {
public:
vector<int> maxInWindows(const vector<int>& num, int size)
{
vector<int> ans;
deque<pair<int, int> > dq;
dq.clear();
ans.clear();
for (int i = 0; i < num.size(); i++) {
if (dq.empty()) dq.push_back(make_pair(num[i], i));
else {
while (!dq.empty() && dq.back().first < num[i]) dq.pop_back();
dq.push_back(make_pair(num[i], i));
while (i - size >= dq.front().second) dq.pop_front();
}
if (!size || i < size - 1) continue;
ans.push_back(dq.front().first);
}
return ans;
}
};
//法二
//单调队列,O(n)
class Solution {
public:
vector<int> maxInWindows(const vector<int>& a, unsigned int k){
vector<int> res;
deque<int> s;
for(unsigned int i = 0; i < a.size(); ++i){
while(s.size() && a[s.back()] <= a[i]) s.pop_back();
while(s.size() && i - s.front() + 1 > k) s.pop_front();
s.push_back(i);
if(k && i + 1 >= k) res.push_back(a[s.front()]);
}
return res;
}
};
- 对于有单调性的问题,例如字典序之类的都可以尝试用这两个数据结构维护,由于每个元素最多操作两次,所以复杂度都是 O ( n ) O(n) O(n),个人觉得这是优先队列牺牲了一些实时性,比如还要统计每个时刻得队列大小之类得,而优化得结构。
- 像单调队列和单调栈只要知道数据规模都可以用数组模拟,能优化常数,操作起来也比较方便
- 这两个结构常用于一些DP方程的优化,例如区间DP、多重背包这类的,像多重背包能够通过单调队列优化到比二进制拆分更优的均摊复杂度
- 单调队列利用到了双端队列,不难发现,在数据结构中这是一种输入受限的双端队列结构,其有着栈和队列的共同优点。在408中比较常考到的就是输入(输出)受限的双端队列单方能得到的序列以及双方都不能得到的序列,解决方法就是枚举,可以利用卡特兰数和排列数检查数量的正确性。
*例如:
1、2、3、4输入序列 - 输入受限双端队列能得到,但输出受限双端队列不能得到的:4、1、3、2
- 输出受限双端队列能得到,但输入受限双端队列不能得到的:4、2、1、3
- 都不能得到的:4、2、3、1