1. 程式人生 > 實用技巧 >資料結構進階:ST表

資料結構進階:ST表

簡介

ST 表是用於解決 可重複貢獻問題 的資料結構。

什麼是可重複貢獻問題?

可重複貢獻問題 是指對於運算 \(\operatorname{opt}\) ,滿足 \(x\operatorname{opt} x=x\) ,則對應的區間詢問就是一個可重複貢獻問題。例如,最大值有 \(\max(x,x)=x\)gcd\(\operatorname{gcd}(x,x)=x\) ,所以 RMQ 和區間 GCD 就是一個可重複貢獻問題。像區間和就不具有這個性質,如果求區間和的時候採用的預處理區間重疊了,則會導致重疊部分被計算兩次,這是我們所不願意看到的。另外, \(\operatorname{opt}\)

還必須滿足結合律才能使用 ST 表求解。

什麼是RMQ?

RMQ 是英文 Range Maximum/Minimum Query 的縮寫,表示區間最大(最小)值。解決 RMQ 問題有很多種方法,如 線段樹 、單調棧、ST表 和 Four Russian -- 基於 ST 表的演算法。

引入

ST 表模板題

題目大意:給定 \(n\) 個數,有 \(m\) 個詢問,對於每個詢問,你需要回答區間 \([l,r]\) 中的最大值。

考慮暴力做法。每次都對區間 \([l,r]\) 掃描一遍,求出最大值。

顯然,這個演算法會超時。

ST 表

ST 表基於 倍增 思想,可以做到 \(\Theta(n\log n)\)

預處理, \(\Theta(1)\) 回答每個詢問。但是不支援修改操作。

基於倍增思想,我們考慮如何求出區間最大值。可以發現,如果按照一般的倍增流程,每次跳 \(2^i\) 步的話,詢問時的複雜度仍舊是 \(\Theta(\log n)\) ,並沒有比線段樹更優,反而預處理一步還比線段樹慢。

我們發現 \(\max(x,x)=x\) ,也就是說,區間最大值是一個具有“可重複貢獻”性質的問題。即使用來求解的預處理區間有重疊部分,只要這些區間的並是所求的區間,最終計算出的答案就是正確的。

如果手動模擬一下,可以發現我們能使用至多兩個預處理過的區間來覆蓋詢問區間,也就是說詢問時的時間複雜度可以被降至 \(\Theta(1)\)

,在處理有大量詢問的題目時十分有效。

具體實現如下:

\(f(i,j)\) 表示區間 \([i,i+2^j-1]\) 的最大值。

顯然 \(f(i,0)=a_i\)

根據定義式,第二維就相當於倍增的時候“跳了 \(2^j-1\) 步”,依據倍增的思路,寫出狀態轉移方程: \(f(i,j)=\max(f(i,j-1),f(i+2^{j-1},j-1))\)

以上就是預處理部分。而對於查詢,可以簡單實現如下:

對於每個詢問 \([l,r]\) ,我們把它分成兩部分: \(f[l,l+2^s-1]\)\(f[r-2^s+1,r]\)

其中 \(s=\left\lfloor\log_2(r-l+1)\right\rfloor\)

根據上面對於“可重複貢獻問題”的論證,由於最大值是“可重複貢獻問題”,重疊並不會對區間最大值產生影響。又因為這兩個區間完全覆蓋了 \([l,r]\) ,可以保證答案的正確性。

模板程式碼

ST 表模板題

#include<bits/stdc++.h>
using namespace std;
const int logn = 21;
const int maxn = 2000001;
int Logn[maxn], f[maxn][logn];
int n, m;

inline int read(){
	int x = 0, f = 1; char ch = getchar();
	while (!isdigit(ch)) { if (ch == '-') f = -1; ch = getchar(); }
	while (isdigit(ch)) { x = x * 10 + ch - 48; ch = getchar(); }
	return x * f;
}

void pre() {
	Logn[1] = 0, Logn[2] = 1;
	for (int i = 3; i < maxn; ++i) 
		Logn[i] = Logn[i / 2] + 1;
}
int main() {
	//freopen("in.txt", "r", stdin);
	//ios::sync_with_stdio(false), cin.tie(0);
	n = read(), m = read();
	for (int i = 1; i <= n; ++i)f[i][0] = read();
	pre();
	//f(i,j) = max(f(i,j - 1),f(i + 1 << (j - 1),j - 1))
	for (int j = 1; j <= logn; j++)
		for (int i = 1; i + (1 << j) - 1 <= n; i++)
			f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
	int x, y;
	while (m--) {
		x = read(), y = read();
		int s = Logn[y - x + 1];
		printf("%d\n", max(f[x][s], f[y - (1 << s) + 1][s]));
	}
}

注意點

  1. 輸入輸出資料一般很多,建議開啟輸入輸出優化。

  2. 每次用 std::log 重新計算 log 函式值並不值得,建議進行如下的預處理:

\[\left\{\begin{aligned} Logn[1] &=0, \\ Logn\left[i\right] &=Logn[\frac{i}{2}] + 1. \end{aligned}\right. \]

ST 表維護其他資訊

除 RMQ 以外,還有其它的“可重複貢獻問題”。例如“區間按位和”、“區間按位或”、“區間 GCD”,ST 表都能高效地解決。

需要注意的是,對於“區間 GCD”,ST 表的查詢複雜度並沒有比線段樹更優(令值域為 \(w\) ,ST 表的查詢複雜度為 \(\Theta(\log w)\) ,而線段樹為 \(\Theta(\log n+\log w)\) ,且值域一般是大於 \(n\) 的),但是 ST 表的預處理複雜度也沒有比線段樹更劣,而程式設計複雜度方面 ST 表比線段樹簡單很多。

如果分析一下,“可重複貢獻問題”一般都帶有某種類似 RMQ 的成分。例如“區間按位與”就是每一位取最小值,而“區間 GCD”則是每一個質因數的指數取最小值。

總結

ST 表能較好的維護“可重複貢獻”的區間資訊(同時也應滿足結合律),時間複雜度較低,程式碼量相對其他演算法很小。但是,ST 表能維護的資訊非常有限,不能較好地擴充套件,並且不支援修改操作。

練習

RMQ 模板題

SCOI2007」降雨量

平衡的陣容 Balanced Lineup


以下摘自網路,僅作為學習演算法使用,侵權刪。

附錄:ST 表求區間 GCD 的時間複雜度分析

在演算法執行的時候,可能要經過 \(\Theta(\log n)\) 次迭代。每一次迭代都可能會使用 GCD 函式進行遞迴,令值域為 \(w\) ,GCD 函式的時間複雜度最高是 \(\Omega(\log w)\) 的,所以總時間複雜度看似有 \(O(n\log n\log w)\)

但是,在 GCD 的過程中,每一次遞迴(除最後一次遞迴之外)都會使數列中的某個數至少減半,而數列中的數最多減半的次數為 \(\log_2 (w^n)=\Theta(n\log w)\) ,所以,GCD 的遞迴部分最多隻會執行 \(O(n\log w)\) 次。再加上迴圈部分(以及最後一層遞迴)的 \(\Theta(n\log n)\) ,最終時間複雜度則是 \(O(n(\log w+\log x))\) ,由於可以構造資料使得時間複雜度為 \(\Omega(n(\log w+\log x))\) ,所以最終的時間複雜度即為 \(\Theta(n(\log w+\log x))\)

而查詢部分的時間複雜度很好分析,考慮最劣情況,即每次詢問都詢問最劣的一對數,時間複雜度為 \(\Theta(\log w)\) 。因此,ST 表維護“區間 GCD”的時間複雜度為預處理 \(\Theta(n(\log n+\log w))\) ,單次查詢 \(\Theta(\log w)\)

線段樹的相應操作是預處理 \(\Theta(n\log x)\) ,查詢 \(\Theta(n(\log n+\log x))\)

這並不是一個嚴謹的數學論證,更為嚴謹的附在下方:

更嚴謹的證明
理解本段,可能需要具備 時間複雜度 的關於“勢能分析法”的知識。

先分析預處理部分的時間複雜度:

設“待考慮數列”為在預處理 ST 表的時候當前層迴圈的數列。例如,第零層的數列就是原數列,第一層的數列就是第零層的數列經過一次迭代之後的數列,即 st[1..n][1] ,我們將其記為 \(A\)

而勢能函式就定義為“待考慮數列”中所有數的累乘的以二為底的對數。即: \(\Phi(A)=\log_2\left(\prod\limits_{i=1}^n A_i\right)\)

在一次迭代中,所花費的時間相當於迭代迴圈所花費的時間與 GCD 所花費的時間之和。其中,GCD 花費的時間有長有短。最短可能只有兩次甚至一次遞迴,而最長可能有 \(O(\log w)\) 次遞迴。但是,GCD 過程中,除最開頭一層與最末一層以外,每次遞迴都會使“待考慮數列”中的某個結果至少減半。即, \(\Phi(A)\) 會減少至少 \(1\) ,該層遞迴所用的時間可以被勢能函式均攤。

同時,我們可以看到, \(\Phi(A)\) 的初值最大為 \(\log_2 (w^n)=\Theta(n\log w)\) ,而 \(\Phi(A)\) 不增。所以,ST 表預處理部分的時間複雜度為 \(O(n(\log w+\log n))\)

其它

文章開源在 Github - blog-articles,點選 Watch 即可訂閱本部落格。 若文章有錯誤,請在 Issues 中提出,我會及時回覆,謝謝。

如果您覺得文章不錯,或者在生活和工作中幫助到了您,不妨給個 Star,謝謝。

(文章完)