单调队列
问题引入:
RMQ(x,y)就是询问数组[x , y]区间内部的最小值
例如:RMQ(0,3)= 1,RMQ(3, 7)= 2
现在,固定询问区间的尾部,例如:RMQ(x,7)
请思考,如下序列中最少记录几个元素,就可以满足RMQ(x, 7)的任何需求
0 1 2 3 4 5 6 7
3 1 4 5 2 9 8 12
最少记录 1、2、8、12即满足RMQ(x ,7)的所有需求,可以发现,这个四个数构成一个单调递增的序列
3 【1】 4 5 【2】 9 【8】【12】
结论:维护一个以j位置为结尾的单调递增序列就可以维护RMQ(i,j)的答案,即以j为结尾的容易区间最小值,再加上区间长度限制,这就是【单调队列】了
单调队列-维护过程:
0 1 2 3 4 5 6 7
3 1 4 5 2 9 8 12
入队操作:
出队操作:
元素性质:
题目演示代码:
给出一个长度为 N N N 的数组,一个长为 K K K 的滑动窗口从最左移动到最右,每次窗口移动,如下图:
找出窗口在各个位置时的极大值和极小值。
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
int main() {
int n, k;
cin >> n >> k;
vector<int> arr;
for (int i = 0; i < n; i++) {
int a;
cin >> a;
arr.push_back(a);
}
deque<int> q;
//递增
for (int i = 0; i < n; i++) {
while (!q.empty() && arr[q.back()] < arr[i]) q.pop_back();//破坏单调性的出队
q.push_back(i);//序列的每个元素都会入队
if (i - q.front() == k) q.pop_front();//如果维护的队列已满,这种情况下队列中元素一定是单调性的,但维护的队列长度下一次要入队新的元素,所以就把队首元素弹出。也是因为这一关系,所以队列中存的是序列索引。
if (i + 1 < k) continue;
cout << " " << arr[q.front()];
}
cout << endl;
q.clear();
//递减
for (int i = 0; i < n; i++) {
while (!q.empty() && arr[q.back()] > arr[i]) q.pop_back();
q.push_back(i);
if (i - q.front() == k) q.pop_front();
if (i + 1 < k) continue;
cout << " " << arr[q.front()];
}
cout << endl;
return 0;
}
单调栈
问题引入:
给一个序列,求序列中,每一个元素左侧,每一个小于它的元素
观察单调队列的逻辑模型,每个需要入队的元素左侧第一个小于它的元素是前一个在队中的元素,根据入队列过程中,每一个元素都入队过,那么将所有元素依次入队,当前元素在队列中的前一个元素,即是问题所求。
这种不从头部出的结构,我们叫它【单调栈】
代码演示:
int main() {
int n;
cin >> n;
vector<int> arr(n);
stack<int> s;
for (int i = 0; i < n; i++) {
cin >> arr[i];
}
for (int i = 0; i < n; i++) {
while (!s.empty() && arr[i] << arr[s.top()]) s.pop();
s.push(i);
}
while (!s.empty()) cout << " " << s[s.top()]; s.pop();
return 0;
}
单调递增:最近小于关系
单调递减:最近大于关系
总结
单调队列:擅长维护区间【最大/最小】值,最小值对应单调递增队列
单调栈:擅长维护最近【大于/小于】关系
题目训练:
单调队列:leetcode的862、1760、1438、512、46、93、45、135、43
单调栈:leetcode的155、496、503、901、739、84、1856、907、42
预告:AVL、KMP、Sunday与shift-[And/or]算法等等金典匹配算法