1. 程式人生 > 其它 >單調佇列學習小記

單調佇列學習小記

最近學習了單調佇列,在此做一個小記


單調佇列(百度百科)維持了佇列的單調性,是佇列的一種,與佇列的區別有兩點:一、單調佇列是單調上升或單調下降的,佇列無要求。二、佇列是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的範圍)}這樣的區間最值問題,就可以考慮用單調佇列去維護。當然,更多的還是要靠刷題鞏固。

到此結束,歡迎指錯!