单调栈与单调队列
单调栈与单调队列
你说我都会树链剖分了,咋还不会单调栈与单调队列啊?
单调栈
问题类型:对于一个长度为 \(n\) 的序列 \(a\),对于 \(\forall 1\le i\le n\),求 \([a_1\sim a_{i-1}]\) 或 \([a_{i+1}\sim a_n]\) 的最靠近 \(i\) 且比 \(a_i\) 大/小的问题。
以在 \(i\) 左边的区间为例。
不过说最值问题可以用线段树、ST 表、树状数组之类的,为啥要学这个嘞?
- 实现简单
- 速度较快,毕竟线段树、树状数组常数都挺大的
- 不会其他的:

那么我们来看看原理。
不妨先手搓一个样例玩玩。
假设数组为 \([1,2,3,4,5]\),求最大值。
那么我们挨个看看 \(i\)。
| \(i\) | 数列 |
|---|---|
| \(1\) | \([1]\) |
| \(2\) | \([1,2]\) |
| \(3\) | \([1,2,3]\) |
| \(4\) | \([1,2,3,4]\) |
| \(5\) | \([1,2,3,4,5]\) |
发现第 \(i\) 次操作有且仅有 \(a_i\) 被新加入数列。即 \([a_1\sim a_{i-1}]\) 是不变的。如果暴力枚举就会重复,效率极低。
所以我们考虑贪心的思想。
因为是求最大值,如果 \(\exist \ i<j,a_i\le a_j\) 的情况,是不是从现在开始,在 \(a_i\) 的有生之年就不可能对答案有贡献。
因为后面的区间如果包含了 \(a_i\),也必定包含了 \(a_j\),所以 \(a_i\) 一定会被 \(a_j\) 所替代,也就无法产生贡献。
那我们就不需要考虑 \(a_i\),可以把它踢出去。
所以我们存储在序列里的元素一定是一个具有单调性的序列。
我们考虑用栈进行存储。
如果 \(i<j,a_i\le a_j\),即 \(a_i\) 已成为废品,那么就让它从栈中弹出。重复执行这一步骤。
最后再加入 \(a_j\) 就算完成了一次操作。
来看看代码。
for(int i=1;i<=n;++i)
{
while(!st.empty()&&h[st.top()]<h[i])
{
ans[st.top()]=i;
st.pop();
}
st.push(i);
}
虽然看起来是双层循环,是 \(O(n^2)\),但是如果仔细算算,发现 while 里面的操作总共最多执行 \(n\) 次,所以总时间复杂度是 \(O(n)\)。
单调队列
单调栈的升级版。升级在于规定了区间长度为 \(m\)。
即当前为 \(i\),与 \(i\) 有关系的区间为 \([i-m-1,i-1]\) 和 \([i+1,i+m+1]\)。求最值。
以在 \(i\) 左边的区间为例。
那么我们再来手搓玩玩。序列还是 \([1,2,3,4,5]\),区间长度为 \(3\)。
| \(i\) | 序列 |
|---|---|
| \(1\) | \([1]\) |
| \(2\) | \([1,2]\) |
| \(3\) | \([1,2,3]\) |
| \(4\) | \([2,3,4]\) |
| \(5\) | \([3,4,5]\) |
其中只有 \(i\in \left\{ 3,4,5\right\}\) 时才有意义。所以只需要考虑 \(3\le i\le 5\)。
可能不够直观,换种方式看看?
注意到,每次改动仅是多加上一个数 \(a_{i+1}\),并减去一个数 \(a_{i-m+1}\)。所以说就是单调栈的升级版。
俗称滑动窗口。感觉十分形象。
这个是真正的区间最值。但上文说过,线段树、ST 表、树状数组都太慢且不容易实现。
老规矩,还是维护一个具有单调性的双端队列,这样可以模拟从尾部进,从队头出。
对于每次“滑动”,先将滑动所加元素加入队列尾部,不断踢出不比其更优的元素。这个原理和单调栈相似。
然后由于是滑动,所以队列前面的元素可能已经超过了窗口,那我们将它踢出队列。
最后将新加元素加到队列尾部。
代码:
for(int i=1;i<=n;++i)
{
while(!dq.empty()&&a[dq.back()]<=a[i])
dq.pop_back();
while(!dq.empty()&&dq.front()<=i-k)
dq.pop_front();
dq.push_back(i);
ans[i]=a[dq.front()];
}
完结撒花。

浙公网安备 33010602011771号