单调队列
Any DP problem where A[i] = min(A[j:k]) + C
where j < k <= i
This is a sliding max/min window problem.
单调队列是一种广泛应用的数据结构,它能够动态地维护定长序列中的最值。可以这么理解,存在有一个队列,队列中的元素需要满足单调性,而且队尾元素和队首元素距离最大是k
。
以递减队列为例子,构造队列的方式如下:
- 入队:待加入的元素值
x
,如果x
小于队尾元素,直接入队;如果x
大于等于队尾元素,则弹出队尾元素,直到队列是空或者队尾元素大于x
,此时x
加入队列 - 出队:对于插入的元素,检查该元素和队首元素之差,如果超过
k
,则弹出队首元素。
注意: 单调队列的长度,不是指元素的个数,而是指队首和队尾元素的距离。我们使用单调队列,是为了在 O ( 1 ) O(1) O(1)的时间内,求出指定区间范围的最大值和最小值。而且,入队和出队是时间有序的,这点是非常重要的。
堆的复杂度是 l o g 2 N log_{2}^{N} log2N,但是堆不能新元素加入的时候,判断是否删除或者删除哪一个,因为堆内部的元素是无序的。平衡树可以,但是结构复杂,复杂度相对来说更高一些,虽然渐进复杂度是一样的。
涉及到时间序列的最值问题,优先考虑单调队列。
单调队列求解滑动窗口
题目赘述,在这里,核心思路是考虑问题的角度。一般来说,我们认为窗口是滑动的,从字面了解也确实是这样。不过我们从另一个角度来看,可以理解为数据流流动进入窗口,终止的时候是数据流的末尾和窗口末尾重叠的时候。这两种情况是等价的。
然后,我们需要一个队列,仅保存那些可能成为最大值的元素。假设我们有了一个队列,里面存储的都是可能成为最大值的元素,则该队列必然是递减的,队头到队尾表示元素进入的时间序列。举个例子,假设此时队列中只有一个元素值是x
,窗口的宽度是w
,此时新加入的元素值是y
。如果y > x
,且y
和x
的距离小于等于w
,则x
是不可能成为最大值的,因为它们此时在一个窗口内,此时应该弹出x
并加入y
。如果队列有多个元素,则一直执行该步骤,直到队列空,或者有元素大于末尾元素。如果y
小于x
,则x
滑出后,y
可能是最大值,如果队列中有其他多个元素也是同理。x
滑出的条件就是,y
和x
的距离大于w
,不论y
是否可能是最大值,或者说,新的元素来临时,新元素和队头元素的距离大于w
。
从另一个角度来看,我们可以任务,滑动窗口是无状态的,已经离开窗口的元素不会对之后的结果造成影响。
#include <iostream>
#include <vector>
#include <deque>
void initArr(std::vector<int>& vec, int N) {
std::cout << "input " << N << " elements:\n";
for (int i = 0; i < N; i++) {
int n;
std::cin >> n;
vec.push_back(n);
}
}
std::vector<int> slideWinMaxNum(const std::vector<int>& input, int w) {
std::vector<int> res;
if (input.empty() || w <= 0 || input.size() < w) { // 边界条件
return res;
}
std::deque<int> maxDep;
for (int i = 0; i < input.size(); i++) {
while (!maxDep.empty() && input[maxDep.back()] <= input[i]) {
maxDep.pop_back();
}
maxDep.push_back(i);
if (maxDep.front() == i - w) {
maxDep.pop_front();
}
if (i >= w - 1) {
res.push_back(input[maxDep.front()]);
}
}
return res;
}
int main() {
int N, w;
std::vector<int> input;
std::cout << "input number of element and window width:\n";
std::cin >> N >> w;
initArr(input, N);
auto&& res = slideWinMaxNum(input, w);
for (const auto& val: res) {
std::cout << val << " ";
}
std::cout << std::endl;
return 0;
}
最终结果:
input number of element and window width:
8 3
input 8 elements:
4 3 5 4 3 3 6 7
5 5 5 4 6 7
复杂度分析,虽然内部嵌套while
循环,但是从优先队列来看,每个元素至多进入队列1次,因此复杂度是
O
(
N
)
O(N)
O(N)
扩展
最大值减去最小值之差小于等于num
的子区间的个数。
#include <iostream>
#include <vector>
#include <deque>
/*
* 基本思路是,利用两个单调队列,在以某个元素作为区间左边界的情况下,
* 不断更替区间范围,并动态获取最值,之后利用最值之差和下标之差,计算
* 符合条件的子数组的个数
*/
int MaxSubMinNum(const std::vector<int>& arr, int num) {
if (arr.empty()) {
return 0;
}
std::deque<int> qmax; // 递减队列
std::deque<int> qmin; // 递增队列
int res = 0;
int i = 0; // 区间左边界
int j = 0; // 区间右边界
int N = arr.size();
while (i < N) {
while (j < N) {
while (!qmax.empty() && arr[qmax.back()] <= arr[j]) {
qmax.pop_back(); // 维护递减队列性质
}
qmax.push_back(j);
while (!qmin.empty() && arr[qmin.back()] >= arr[j]) {
qmin.pop_back(); // 维护递增队列性质
}
qmin.push_back(j);
if (qmax.front() - qmin.front() >= num) {
break; // 找到满足性质的区间,不再需要递增区间右边界范围
}
++j;
}
if (qmax.front() == i) {
qmax.pop_front(); // 以arr[i]为边界的区间查找结束
}
if (qmin.front() == i) {
qmin.pop_front();
}
res += (j - i); // arr[i]为边界的个数
i++; // 移动窗口右边界
}
}
int main() {
return 0;
}
参考文献
- https://ksmeow.moe/monotonic_queue/
- https://endlesslethe.com/monotone-queue-and-stack-tutorial.html
- https://carecraft.github.io/basictheory/2018/12/monotone_stack/
- https://ruanx.pw/post/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97.html
- https://leetcode.com/problems/shortest-subarray-with-sum-at-least-k/discuss/204290/Monotonic-Queue-Summary