单调队列和单调栈都是维护单调性的线性数据结构。
如果了解过 RMQ 的同学可能知道,大部分 RMQ 数据结构的区间查询复杂度都是 \(O(\log n)\) 级别的。但是单调队列和单调栈却能在 \(O(1)\) 的时间内完成类似操作。
想要达成这一点,要利用队列和栈的结构做文章。
达成单调性
单调队列和单调栈的单调性实际上是结构内的单调性,而且这种单调性是双重的。
第一重单调性是 值的单调性。这个很好理解,就是结构内的元素的值按照一定的顺序排列。
第二重单调性则是 过期时间的单调性,这就是单调队列和单调栈的关键所在。
每一个元素在插入结构时,都「预定」了一个过期时间。这个时间并不会改变,而且与插入时间正相关。
补充:「正相关」的意思就是「一个越……,另一个也越……」,或者说如果 \(x_i<x_j\),那么 \(y_i<y_j\)。
为了维护这双重单调性,结构内必然要对元素进行删减。
不论是单调队列还是单调栈,都使用正面开口进行插入和元素调整。不同之处仅在于单调队列用后面的开口进行删除,而单调栈使用正面。接下来就使用单调队列进行演示。
首先,队列内原本保持双重单调性,但是新的元素插入,就会导致单调性被破坏:
下一步,就是将在队列前面的,不如这个元素优的元素弹出,然后再插入:
如果一个人比你小还比你强,那么你就永远超不过 Ta 了。 —— chen_zhe
常用模型
下面介绍一些常用的单调队列/单调栈应用。
滑动窗口
顾名思义,这就是一个框定大小进行的 RMQ。
使用单调队列可以很容易解决。
限定区间大小的最大/最小区间和
这也是单调队列的经典应用。
求出前缀和以后,对于每一个区间末尾,寻找一个区间开头就是 RMQ。
单调栈内二分
这是一个比较冷门的技巧。常用于限定末尾,没有限定开头,且末尾需要插入的区间查询问题。
具体请看 【题解】最大数(咕咕咕)。
例题
滑动窗口
单调队列模板题。
#include <cstdio>
#include <cctype>
#include <queue>
using namespace std;
const int max_n = 1000000;
struct item
{
int ind, val;
item(int _i = 0, int _v = 0) : ind(_i), val(_v) { }
};
deque<item> mx, mn;
int res1[max_n], res2[max_n];
inline int read()
{
int ch = getchar(), n = 0, t = 1;
while (isspace(ch)) { ch = getchar(); }
if (ch == '-') { t = -1, ch = getchar(); }
while (isdigit(ch)) { n = n * 10 + ch - '0', ch = getchar(); }
return n * t;
}
int main()
{
mx.clear();
mn.clear();
int n = read(), k = read(), tmp;
for (int i = 0; i < n; i++)
{
tmp = read();
while (!mx.empty())
{
if (mx.front().ind <= i - k)
mx.pop_front();
else
break;
}
while (!mn.empty())
{
if (mn.front().ind <= i - k)
mn.pop_front();
else
break;
}
while (!mx.empty())
{
if (mx.back().val <= tmp)
mx.pop_back();
else
break;
}
mx.emplace_back(i, tmp);
while (!mn.empty())
{
if (mn.back().val >= tmp)
mn.pop_back();
else
break;
}
mn.emplace_back(i, tmp);
if (i >= k - 1)
{
res1[i-k+1] = mx.front().val;
res2[i-k+1] = mn.front().val;
}
}
for (int i = k; i <= n; i++)
printf("%d ", res2[i-k]);
putchar('\n');
for (int i = k; i <= n; i++)
printf("%d ", res1[i-k]);
putchar('\n');
return 0;
}
琪露诺
这就是不折不扣的用单调队列优化 DP 的模板题。
定义 \(f_i\) 为琪露诺跳到第 \(i\) 格的最大值。一步从 \(i\) 跳到 \([i+L,i+R]\),相当于 \(f_{i^{\prime}}\) 从 \(f_{i^{\prime}-R}\) 到 \(f_{i^{\prime}-L}\) 之间转移。
因为 \(v_i\) 是固定的,所以相当于一个 RMQ,使用单调队列解决。最后再求一个最大值就好了。
切蛋糕
这是一个相当经典的模型——限定区间大小的最大/最小区间和。这也是用单调队列解决的。
我们先求出前缀和,然后遍历。对于每一个位置 \(i\),相当于求 \(S_{i-k}\ (1\le k\le m)\) 的极值,其中 \(m\) 是大小限制。
然后,用 \(S_i-S_u\) 更新答案就可以了,\(S_u\) 是极值。注意这里的最大/最小要与区间和要求的最小/最大相反(毕竟要减掉嘛)。
Largest Rectangle in a Histogram
这是另一个经典的模型——最大折线下正方形。(咕咕咕)