# 單調佇列 單調佇列是單調性的一種運用,透過觀察在過濾掉一些不可能、或不需考慮的元素後,留下來的正好會是一個具單調性的序列時,利用其單調性來優化計算量的一種方法。 以下透過例題來理解單調佇列通常用在什麼地方,以及如何使用。 :::success ### CSES 1645 - Nearest Smaller Values https://cses.fi/problemset/task/1645/ ::: 這題的暴力解非常直覺:對每個元素往左找到第一個比自己小的元素,停下來並輸出它。 不過,考慮邪惡一點的測資:一個遞減的序列。這會導致每個元素的答案都是 $0$ 而且都必須找到底才能判斷,最糟計算量 $O(N^2)$。 順序是重要的,所以也不能排序再來二分搜尋。 ### 觀察 在這題,因為答案本身就有 $N$ 個,窮舉每個元素是省不掉的。那麼,是否有辦法減少一些候補呢? 先假設每個元素必定相異(相同的情況可以等分析完再來考慮),任取兩個元素 $A_i$ 和 $A_j$,這裡 $i<j$。對於所有在 $A_j$ 右邊的元素,這兩個同時都是候補。 :::info 只要考慮好 $2$ 個元素的情況,就能推廣至任意數量的情況。 對於候補超過 $2$ 個元素的情況,可以兩兩拿出來套 $2$ 個元素時的結論。 ::: 它們之間的關係只有兩種: - $A_i<A_j$ - $A_i>A_j$ 考慮第 $1$ 種情況,在 $A_i<A_k<A_j$ 時,$A_i$ 仍會是 $A_k$ 的所求,因此 $A_j$ 並不可能完全覆蓋 $A_i$ 的客群;而 $A_k>A_j>A_i$ 時會選較近的 $A_j$,所以 $A_i$ 也不可能完全覆蓋 $A_j$ 的客群。那麼兩者都有保留的必要。 ![](https://i.imgur.com/g80OHhI.png) 考慮第 $2$ 種情形,任何可能會選到 $A_i$ 的數 $A_k>A_i$($k>j$)基於遞移律也必定滿足 $A_k>A_j$,但是 $A_j$ 更靠近右邊的 $A_k$,所以 $A_i$ 是不可能被選上的,它的客群完全被 $A_j$ 覆蓋。 因此在 $A_j$ 出現後,$A_i$ 就不再可能是任何人的所求。可以被 $A_j$ 剔除掉。 ![](https://i.imgur.com/rygBdFd.png) 由此結論,對任意數量的候補,只要存在任意數對 $(i, j)$ 滿足 $i<j$ 且 $A_i>A_j$,此時 $A_i$ 可被刪去而不影響後續答案。 :::info 這裡回應了前述的「只需考慮 $2$ 個元素間的情況」,就能推廣至任意數量的元素,透過任取 $2$ 個來套用 $2$ 個元素時的結論,來排除不必要的候補。 ::: 刪到最後,必不存在任何數對 $(i, j)$ 滿足 $i<j$ 且 $A_i>A_j$。換句話說,對任意數對 $(i, j)$ 滿足 $i<j$ 時,一定也滿足 $A_i<A_j$,可知剩下的候補形成的序列為遞增序列,具備單調性。 :::warning 而 $A_i=A_j$ 的情況,比對會發現更接近第 $2$ 種情況:$A_i$ 客群被完全覆蓋。因此可將相等的情況歸類至第 $2$ 種情況。因此剩下的候補會是嚴格遞增序列。 ::: 考慮新進的 $A_k$,對所有可能的候補可依照和 $A_k$ 之間的大小關係分成兩邊: ![](https://i.imgur.com/TbxrFx7.png) 右邊大於等於 $A_k$ 的部份,在 $k+1$ 以後客群會被 $A_k$ 完全覆蓋,所以不再需要保留;左邊小於 $A_k$ 的部份繼續留著,且最右側的元素即為比 $A_k$ 小、又最靠近 $A_k$ 的元素,即為所求。最後將 $A_k$ 置於所求的下一個位置。 這個分界線可利用單調性進行二分搜尋,不過因為之後會刪除掉,所以直接窮舉也可以。 每個元素只會被加進候補 $1$ 次,被窮舉並移除 $1$ 次,整體的複雜度為 $O(N)$,反而比每次二分搜尋來得有效率,且易於實作。 實作上由於只會從最右邊開始移除或插入,因此使用 stack 即可維護。 ```cpp= // 因為常用到 stack 最末尾元素,定義為最後一個元素所在位置比較方便 int tail = 0; // 用來擋 stack 為空時的特例 val[0] = -1e9; // 需要記錄位置用來輸出 idx[0] = 0; for (int i=1; i<=n; i++) { // 排除所有比 ary[i] 大的元素 for (; tail>0&&ary[i]>=val[tail]; --tail); // 排除完後剩下都比 ary[i] 小,此時 tail 是最右邊最靠近的 ans[i] = idx[tail]; // 把 ary[i] 作為新候補塞進 stack ++tail; val[tail] = ary[i]; idx[tail] = i; } ``` :::success ### ZJ f174 - m6a2-蛋糕(Cake) https://zerojudge.tw/ShowProblem?problemid=f174 ::: 直觀的暴力解同樣非常單純,只要窮舉所有的區間起點,再窮舉 $1$ 到 $K$ 之間所有長度,可得 $O(NK)$ 的做法,不過顯然不夠有效率。 由於存在負數,也不能保證區間長度與區間和的關係,固定起點時的區間和也不存在單調性,反例從 sample 就能找到。 ## 觀察 考慮固定區間終點 $t$ 的情況,尋找最大區間和。為了找出可用的特性,嘗試把區間和用其它方式改寫來觀察看看,例如前綴和 $S$,則所求可寫成: $$ \max\begin{cases} S_t-S_{t-K}\\ S_t-S_{t-K+1}\\ \ldots\\ S_t-S_{t-1}\\ \end{cases} $$ 由於 $S_t$ 固定,可以提出來: $$ S_t-\min\begin{cases} S_{t-K}\\ S_{t-K+1}\\ \ldots\\ S_{t-1}\\ \end{cases} $$ 問題被轉成求區間最小值,即經典 RMQ 問題,但這問題太難,所以先考慮是否有機會刪減一些候補。 ## 刪減候補 考慮兩個元素 $S_i$ 和 $S_j$ 的情形,先設定 $i<j$: - $S_i<S_j$ - $S_i>S_j$ 考慮第 $1$ 種情形,平常應該要選較小的 $S_i$,但是因為 $S_i$ 比較靠左,如果到 $S_{i+K}$ 以後就取不到了,這時 $S_j$ 就算比較大,至少還是可選的。 考慮第 $2$ 種情形,$S_i$ 比較大所以不會被選中,但要等 $S_j$ 離太遠時,$S_i$ 會離更遠。所以 $S_i$ 沒有任何情況能夠被選上,$S_j$ 完全覆蓋。因此把 $S_i$ 刪掉也不影響結果。 相等的情況和第 $2$ 種情況一樣,可視為完全覆蓋。 因此,把不可能被選中的候補刪去的話,剩下的候補會是嚴格遞增序列。 和前一題不同的是,所求為最小值,而遞增序列的最小值位於最左邊。另一處不同點是左側候補有可能因為距離超過 $K$ 而被淘汰。 求遞增序列中最小值 $O(1)$,每個元素被放進單調佇列 $1$ 次,被刪去 $1$ 次,總複雜度為 $O(N)$。 實作上兩頭都會變動,因此兩頭都需要記錄。 ```cpp= // head, tail 頭尾包含 int head = 0, tail = 0, ans = 0; val[0] = 0; idx[0] = 0; for (int i=1; i<=n; i++) { // 排除距離超過 k 的部份 if (idx[head] <= i-k) { ++head; } // 遞增序列最小值即為序列最左側元素 ans = max(ans, sum[i]-val[head]); // 排除所有比 sum[i] 大的元素 (sum 為前綴和) for (; tail>=head&&sum[i]<=val[tail]; --tail); // 把 sum[i] 作為新候補塞回去 ++tail; val[tail] = sum[i]; idx[tail] = i; } ``` :::success ### TIOJ 1368 - Get High!! https://tioj.ck.tp.edu.tw/problems/1368 ::: 很直觀地窮舉所有可能的區間,可以得到一個 $O(N^2)$ 的解,但顯然不夠快。 固定終點的話,儘管序列全是正整數,區間長度越長總和越大,但區間最小值也可能更小,因此不具備單調性。 不過,考慮區間最小值不變的前提,還是具備單調性的,在此前提下區間越長越好。 因此,可以得到一個結論:最小值相同時,區間長度越長,心情就越好。 而最小值改變的前提是區間往左右擴張時,加進比目前最小值更小的值。所以對每個元素,若能找到兩邊比自己小的值中,最靠近自己的,這兩個值就各會是維持最小值不變時,左右的擴張極限。 ![](https://i.imgur.com/Af6tf5k.png) 如上圖,以塗藍色的部份為最小值時,往兩邊擴張到撞到小於自己的值為止,如圖中的藍線所示,這段區間就是這個最小值的最佳解。窮舉每個元素作為最小值,取算出來的結果最大的,即為所求。 此時問題轉為如何快速對每個最小值 $A_i$ 求左右邊界,那麼必須先找出邊界的條件。以左邊界為例,要找到左邊比自己小的數中最靠近自己的,即找到最大的 $l$ 使得 $l<i$ 且 $A_l<A_i$。 有注意到什麼嗎?沒錯,轉換後的問題和本篇講解的第一題(CSES 1645)所求一模一樣。右邊界可以用相同方式求得,或者將序列反轉後當作左邊界來求也可以。 於是得到在 $2$ 次的 $O(N)$ 求出每個最小值的最遠左、右邊界的方法,對每個最小值利用前綴和可以 $O(1)$ 算出區間的心情,整體複雜度為 $O(N)$。 ### 另解 同樣考慮每個元素作為最小值的情形,對任意元素 $A_i$ 和 $A_j$ 在假定 $i<j$ 時的關係: - $A_i<A_j$ - $A_i>A_j$ 在第 $1$ 種情況,對 $A_i$ 而言,$A_j$ 不會妨礙到它往右延伸,且在當前元素 $A_k$ 處於 $A_i<A_k<A_j$ 時,$A_k$ 會被 $A_i$ 卡住但不會被 $A_j$ 卡住,所以 $A_i$ 還有被考慮的餘地,向右也還有機會延伸。 但在第 $2$ 種情況,$A_i$ 已被 $A_j$ 截斷,不可能再向右伸延;而若當前元素 $A_k>A_i$ 時,基於遞移律也會滿足 $A_k>A_j$ 所以會先被 $A_j$ 截斷,因此 $A_i$ 完全沒有任何意義,可被刪去而不影響結果。 因此只留下有意義的候補時,必為遞增序列。在加進新元素 $A_k$ 時,對於那些比 $A_k$ 大的元素 $A_j$,相當於決定了它們的右邊界,所以在刪去時可以順便結算並更新答案;直到第一個比 $A_k$ 小的元素出現時,則決定了 $A_k$ 的左邊界。 也就是說,每個元素在被加進序列時會找到左邊界,被移出序列時會決定右邊界,故移出時可順便結算。 :::warning 為了便於計算,可在最左邊加進高度 $-1$ 的元素,就不用特判如果把所有東西移除時左邊界該怎麼辦;每個元素在被移出序列時才結算,為了不在結束後還必須記得多寫一份重複的邏輯來移除剩下的元素,可在最右邊加進高度 $0$ 的元素,就會自然結算掉所有剩餘的元素。 為了不讓最右邊移除掉最左邊,所以最左用 $-1$ 最右用 $0$。這種追加原本不存在的 dummy 來簡化實作邏輯的技巧非常實用。 ::: 這個做法只要 $1$ 次的 $O(N)$ 計算量,即可算出答案。 :::success ### UVa 836 - Largest Submatrix http://domen111.github.io/UVa-Easy-Viewer/?836 ::: 這題有蠻多做法能 AC 的,畢竟 $N\le 25$ 隨便做都能過。 :::danger 這題看起來沒給矩陣大小的範圍,完全是中譯的問題,切到原文就會看到很明確的規定了 $1\le N\le 25$。~~不過沒講最多幾組 test cases~~ ::: 一個單純的想法是窮舉所有可能的矩陣上下界,對每個上下界窮舉每個直行是否在範圍內全都是 $1$,找到連續最多的直行,就是符合這個上下界的最大矩形。上下界 $O(N^2)$ 每個上下界窮舉所有直行 $O(N)$ 整體複雜度 $O(N^3)$。 ## 問題轉換 若嘗試固定矩形下緣,找到貼著這個下緣的所有矩形最大面積,則只要窮舉 $N$ 種下緣。決定下緣後,每個直行先算出貼著此下緣的最大高度,會相當於是 $N$ 個寬度 $1$ 的矩形並排在地面上。 ![](https://i.imgur.com/HruQ0Ex.png) 看起來就像這樣,是不是好像有點印象? 觀察會發現若決定將矩形左、右界定在 $[l, r]$ 則最大面積會是 $(r-l+1)\times\min(A_l, A_{l+1},\ldots,A_r)$,即寬度乘上區間內最小高度。 是不是和上一題的「區間內最小值 $\times$ 區間加總」有那麼點像? 於是發現可以套用幾乎相同的做法:在放進序列時找左界,移除時決定右界並結算,可在 $O(N)$ 的計算量,算出貼齊給定下緣的最大矩形面積。需要窮舉的下緣有 $N$ 種,總複雜度為 $O(N^2)$。 ## 歡樂練習時間 :::success ### CSES 1644 - Maximum Subarray Sum II https://cses.fi/problemset/task/1644/ ::: :::success ### TIOJ 1176 - Cows https://tioj.ck.tp.edu.tw/problems/1176 ::: :::success ### TIOJ 1618 - 城市景觀問題 https://tioj.ck.tp.edu.tw/problems/1618 ::: :::success ### TOJ 169 - 桿杆可見度問題 II https://toj.tfcis.org/oj/pro/169/ ::: :::success ### UVa 11488 - Hyper Prefix Sets http://domen111.github.io/UVa-Easy-Viewer/?11488 ::: :::success ### ZJ c364 - 我鄙視你 https://zerojudge.tw/ShowProblem?problemid=c364 ::: {%hackmd @sa072686/__style %} ###### tags: `競程:二章後半`, `競程`