1. 程式人生 > 實用技巧 >【學習筆記】單調佇列 & 單調棧

【學習筆記】單調佇列 & 單調棧

單調佇列和單調棧都是維護單調性的線性資料結構。

如果瞭解過 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

這是另一個經典的模型——最大折線下正方形。(咕咕咕)