单调栈与单调队列
单调栈与单调队列
你说我都会树链剖分了,咋还不会单调栈与单调队列啊?
单调栈
问题类型:对于一个长度为 nnn 的序列 aaa,对于 ∀1≤i≤n\forall 1\le i\le n∀1≤i≤n,求 [a1∼ai−1][a_1\sim a_{i-1}][a1∼ai−1] 或 [ai+1∼an][a_{i+1}\sim a_n][ai+1∼an] 的最靠近 iii 且比 aia_iai 大/小的问题。
以在 iii 左边的区间为例。
不过说最值问题可以用线段树、ST 表、树状数组之类的,为啥要学这个嘞?
- 实现简单
- 速度较快,毕竟线段树、树状数组常数都挺大的
- 不会其他的:
那么我们来看看原理。
不妨先手搓一个样例玩玩。
假设数组为 [1,2,3,4,5][1,2,3,4,5][1,2,3,4,5],求最大值。
那么我们挨个看看 iii。
iii | 数列 |
---|---|
111 | [1][1][1] |
222 | [1,2][1,2][1,2] |
333 | [1,2,3][1,2,3][1,2,3] |
444 | [1,2,3,4][1,2,3,4][1,2,3,4] |
555 | [1,2,3,4,5][1,2,3,4,5][1,2,3,4,5] |
发现第 iii 次操作有且仅有 aia_iai 被新加入数列。即 [a1∼ai−1][a_1\sim a_{i-1}][a1∼ai−1] 是不变的。如果暴力枚举就会重复,效率极低。
所以我们考虑贪心的思想。
因为是求最大值,如果 ∃ i<j,ai≤aj\exist \ i<j,a_i\le a_j∃ i<j,ai≤aj 的情况,是不是从现在开始,在 aia_iai 的有生之年就不可能对答案有贡献。
因为后面的区间如果包含了 aia_iai,也必定包含了 aja_jaj,所以 aia_iai 一定会被 aja_jaj 所替代,也就无法产生贡献。
那我们就不需要考虑 aia_iai,可以把它踢出去。
所以我们存储在序列里的元素一定是一个具有单调性的序列。
我们考虑用栈进行存储。
如果 i<j,ai≤aji<j,a_i\le a_ji<j,ai≤aj,即 aia_iai 已成为废品,那么就让它从栈中弹出。重复执行这一步骤。
最后再加入 aja_jaj 就算完成了一次操作。
来看看代码。
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(n2)O(n^2)O(n2),但是如果仔细算算,发现 while
里面的操作总共最多执行 nnn 次,所以总时间复杂度是 O(n)O(n)O(n)。
单调队列
单调栈的升级版。升级在于规定了区间长度为 mmm。
即当前为 iii,与 iii 有关系的区间为 [i−m−1,i−1][i-m-1,i-1][i−m−1,i−1] 和 [i+1,i+m+1][i+1,i+m+1][i+1,i+m+1]。求最值。
以在 iii 左边的区间为例。
那么我们再来手搓玩玩。序列还是 [1,2,3,4,5][1,2,3,4,5][1,2,3,4,5],区间长度为 333。
iii | 序列 |
---|---|
111 | [1][1][1] |
222 | [1,2][1,2][1,2] |
333 | [1,2,3][1,2,3][1,2,3] |
444 | [2,3,4][2,3,4][2,3,4] |
555 | [3,4,5][3,4,5][3,4,5] |
其中只有 i∈{3,4,5}i\in \left\{ 3,4,5\right\}i∈{3,4,5} 时才有意义。所以只需要考虑 3≤i≤53\le i\le 53≤i≤5。
可能不够直观,换种方式看看?
[1 2 3] 4 5 1 [2 3 4] 5 1 2 [3 4 5]
[1\ 2\ 3]\ 4\ 5\\
\ 1\ [2\ 3\ 4]\ 5\\
\ 1\ 2\ [3\ 4\ 5]\\
[1 2 3] 4 5 1 [2 3 4] 5 1 2 [3 4 5]
注意到,每次改动仅是多加上一个数 ai+1a_{i+1}ai+1,并减去一个数 ai−m+1a_{i-m+1}ai−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()];}
完结撒花。