1. 程式人生 > >單調隊列 —— 滑動窗口(滾動最大值)

單調隊列 —— 滑動窗口(滾動最大值)

簡單 保持 工作 影響 區間最值 因此 for 開始 +=

一道經典的單調隊列題目——[洛谷P1886 滑動窗口]。(下文開始只討論求滾動的最大值)

暴力解法是O(n^2)的,對於每一個起點,搜一遍長度為k的子序列,求得最值——復雜度不知高到哪裏去了

考慮O(n)的解法:

每一次我們求解完一個區間,僅僅只是向右挪了一個單位,也就是說左邊刪掉了一個元素,右邊加上了一個元素,變動是很小的——除非被刪掉的那一個元素是上一輪的最大值,否則最大值肯定要麽不變,要麽是新加進來的那個。而就是因為上一輪的最大值也許(肯定有一天)會被刪掉,所以讓我們這種保存上一輪最大值的方法不成立了。那是不是意味著我們要保存每一輪的次大值?那如果次大值也被刪掉了,那麽還要保存第三大的……

這樣來推理,我們要保存的是一個單調遞減的隊列,支持在兩端刪除和插入。這就是我們要介紹的一種單調線性數據結構——單調隊列。在題目要我們求解左右邊界同時向同一個方向移動的區間最值問題時,一般都會用到單調隊列。(擴展說明:當我們推出的dp方程只與一個特定的位置x及一些常量有關時,可以用單調隊列來優化)

單調隊列的插入(推入)

沿著上面的思路,我們來考慮如何插入一個數。當我的窗口往右移動一個單位的時候,會有一個元素加進來——如果這個元素要比隊尾的元素大,就無法保證隊列單調遞減了。因此我們猜想,可以不停刪除隊尾的元素,直到這個元素插入之後隊列顯得單調。

這樣的做法的正確性如何證明呢?首先我們來考慮,插入時被我們踢出的那些元素是什麽?是上一輪中保存下來的一些值,並且不是最大值——把他們刪掉完全沒有影響。

為什麽這個會完全沒有影響?我們考慮對於同一個區間的同一個最大值:肯定是越靠後越好。因為這樣它作為最大值被利用的次數肯定更多。(窗口不斷右移,如果最大值越靠左就越早出界)。所以既然被踢出的那些元素位置比我靠左,值還比我小,那留著他們幹嘛用呢?它們以後永遠不會被用到,完全沒有利用價值。因此我們得到了單調隊列插入元素的方法:不斷踢出隊尾元素,直到當前數字插入後讓隊列仍然保持單調。

單調隊列的刪除(踢出)

再來考慮單調隊列的刪除——其實刪除很簡單。我們對單調隊列的利用僅僅只是取隊頭,因為隊頭即為最值。所以如果隊頭不在目前考慮的區間內,就應該把隊頭扔掉。直到隊頭在區間內為止——而此時因為隊列是單調的,所以隊頭一定是符合區間位置的最值。

總結一下,單調隊列工作的框架:

for(...){

   while(...) 踢出隊尾的影響單調性的

  推入隊尾

  while(...) 踢出隊頭的不合法的

  得到隊頭作為當前答案

}

其中,元素的推入常常單獨寫一個push函數。

代碼實現:

inline void Push(int x){
    while(h <= t && a[x] > a[q[t]]) --t;
    q[++t] = x;
}
for(int i = k; i <= n; ++i){
    Push(i);
    if(i > k){
        while(h <= t && q[h] < i-k+1) ++h;
    }
    ans += a[q[h]];
}

單調隊列 —— 滑動窗口(滾動最大值)