【學習筆記】單調佇列 & 單調棧
單調佇列和單調棧都是維護單調性的線性資料結構。
如果瞭解過 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
這是另一個經典的模型——最大折線下正方形。(咕咕咕)