單調佇列學習小記
最近學習了單調佇列,在此做一個小記
單調佇列(百度百科)維持了佇列的單調性,是佇列的一種,與佇列的區別有兩點:一、單調佇列是單調上升或單調下降的,佇列無要求。二、佇列是tail進,head出,而單調佇列是tail進,但head和tail都可以出(如圖)。
舉個栗子,如果一個佇列中有1,4,7,13這幾個數字,這時候又來了一個5。為了維持佇列的單調性,我們要先把大於5的7和13彈出,在將5壓入,佇列就變成了1,4,5。單調佇列是以比較為基礎操作的,時間複雜度是線性的。具體看模板題。
洛谷 P1886 滑動視窗 /【模板】單調佇列
題目描述
有一個長為 n 的序列 a,以及一個大小為 k 的視窗。現在這個從左邊開始向右滑動,每次滑動一個單位,求出每次滑動後窗口中的最大值和最小值。
例如:
The array is [1,3,−1,−3,5,3,6,7], and k = 3。
輸入格式
輸入一共有兩行,第一行有兩個正整數 n,k。 第二行 n 個整數,表示序列 a
輸出格式
輸出共兩行,第一行為每次視窗滑動的最小值
第二行為每次視窗滑動的最大值
輸入輸出樣例
輸入 #1
8 3
1 3 -1 -3 5 3 6 7
輸出 #1
-1 -3 -3 -3 3 3
3 3 5 5 6 7
說明/提示
【資料範圍】
1≤k≤n≤10^6, -231≤a[i]≤231
看到這道題最先想到的必然是暴力,時間複雜度為O(nk),明顯會超時。
這道題是單調佇列的模板題。以找最大值為例,如果有兩個點,下標分別為i和j。如果i>j且a[i]>a[j],那麼這個j就毫無用處了。因為區間是向右滑動的,所以無論如何j也不可能是最大值了。相反的,如果i<j但是a[i]<a[j]呢?這時,j是需要被保留的,因為i可能不在下個區間了。因此,在每個區間判斷前,都要先把隊頭的不在這個區間內的彈出。以上就是思路,下面附上程式碼:
#include<cstdio> #include<iostream> #include<cstring> #define MAXN 1000000 using namespace std; int read() { int sum = 0, f = 1; char ch = getchar(); while(!isdigit(ch)) { if(ch == '-') { f = -1; } ch = getchar(); } while(isdigit(ch)) { sum = sum * 10 + ch - '0'; ch = getchar(); } return sum * f; } int a[MAXN + 9]; int q[MAXN + 9]; int n, k; void min() { int head = 1, tail = 0; for(int i = 1; i <= n; ++i) { while(head <= tail && q[head] + k < i + 1) head++; while(head <= tail && a[i] < a[q[tail]]) tail--; q[++tail] = i; if(i >= k) printf("%d ", a[q[head]]); } } void max() { int head = 1, tail = 0; for(int i = 1; i <= n; i++) { while(head <= tail && q[head] + k < i + 1) head++; while(head <= tail && a[i] > a[q[tail]]) tail--; q[++tail] = i; if(i >= k) printf("%d ", a[q[head]]); } } int main() { n = read(), k = read(); for(int i = 1; i <= n; ++i) { a[i] = read(); } min(); printf("\n"); memset(q, 0, sizeof(q)); max(); return 0; }
單調佇列主要用以處理一些區間求最值得問題,下面就來看一個簡單地應用。
洛谷 P1714 切蛋糕
題目描述
今天是小 Z 的生日,同學們為他帶來了一塊蛋糕。這塊蛋糕是一個長方體,被用不同色彩分成了 n 個相同的小塊,每小塊都有對應的幸運值。
小 Z 作為壽星,自然希望吃到的蛋糕的幸運值總和最大,但小 Z 最多又只能吃 m(m≤n) 小塊的蛋糕。
請你幫他從這 n 小塊中找出連續的 k(1≤k≤m) 塊蛋糕,使得其上的總幸運值最大。
輸入格式
第一行兩個整數 n,m。分別代表共有 n 小塊蛋糕,小 Z 最多隻能吃 m 小塊。
第二行 n 個整數,第 i 個整數 p[i] 代表第 i 小塊蛋糕的幸運值。
輸出格式
僅一行一個整數,即小 Z 能夠得到的最大幸運值。
輸入輸出樣例
輸入 #1
5 2
1 2 3 4 5
輸出 #1
9
輸入 #2
6 3
1 -2 3 -4 5 -6
輸出 #2
5
說明/提示
【資料範圍】
1≤n≤5×10^5,∣p[i]∣≤500。
保證答案在int範圍以內,且為非負整數。
簡化一下問題,就是求最大不定長欄位和的問題。設f[i]為以i為右端點的最大欄位和,則f[i]=max{sum[i]-sum[j] (i-m≤j≤i-1)}。其中sum為字首和預處理,這點不難想到。觀察發現,sum[i]始終為定值,所以max{sum[i]-sum[j] (i-m≤j≤i-1)}可以轉化為sum[i]-min{sum[j] (i-m≤j≤i-1)}。因為每一次j的迴圈都花了非常多的時間,時間複雜度為O(nm),所以資料一大就會T掉,因此考慮用單調佇列維護min{sum[j] (i-m≤j≤i-1)}。以上即為基本思路,下面附上程式碼:
#include<cstdio>
#include<iostream>
#define MAXN 500000
using namespace std;
int read() {
int f = 1, sum = 0;
char ch = getchar();
while(!isdigit(ch)) {
if(ch == '-') f = -1;
ch = getchar();
}
while(isdigit(ch)) {
sum = sum * 10 + ch - '0';
ch = getchar();
}
return f * sum;
}
struct Q {
int id, sum;
};
Q q[MAXN * 2 + 9];
int sum[MAXN + 9];
int main() {
int n = read(), m = read();
for(int i = 1; i <= n; ++i) {
sum[i] = read();
sum[i] += sum[i - 1];
}
int head = 1, tail = 0;
q[++tail].id = 0;
q[tail].sum = 0;
int ans = - 1e9;
for(int i = 1; i <= n; ++i) {
while(head <= tail && q[head].id < i - m) {
++head;
}
while(head <= tail && q[tail].sum >= sum[i]) {
--tail;
}
ans = max(ans, sum[i] - q[head].sum);
q[++tail].id = i;
q[tail].sum = sum[i];
}
printf("%d", ans);
return 0;
}
以上即為單調佇列的應用例子。單調佇列使用頻率不高,但每次使用都會有意想不到的作用,在OI/ACM中也是各種神奇演算法的基礎。在題目中,若出現形同max{sum[j] (j的範圍)}、min{sum[j] (j的範圍)}這樣的區間最值問題,就可以考慮用單調佇列去維護。當然,更多的還是要靠刷題鞏固。