Sparse Table

RonChen發表於2024-10-18

Sparse Table 可用於解決這樣的問題:給出一個 \(n\) 個元素的陣列 \(a_1, a_2, \cdots, a_n\),支援查詢操作計算區間 \([l,r]\) 的最小值(或最大值)。這種問題被稱為區間最值查詢問題(Range Minimum/Maximum Query,簡稱 RMQ 問題)。預處理的時間複雜度為 \(O(n \log n)\),預處理後陣列 \(a\) 的值不可以修改,一次查詢操作的時間複雜度為 \(O(1)\)

例題:P2880 [USACO07JAN] Balanced Lineup G

有一個包含 \(n\) 個數的序列 \(h_i\),有 \(q\) 次詢問,每次詢問 \(h_{a_i}, h_{a_i + 1}, \cdots, h_{b_i - 1}, h_{b_i}\) 中最大值與最小值的差。
資料範圍:\(1 \le n \le 5 \times 10^4, \ 1 \le q \le 1.8 \times 10^5, \ 1 \le h_i \le 10^6, \ a_i \le b_i\)

分析:題目要求最大值和最小值的差難以直接求出,通常需要分別求解最大值和最小值。最直接的做法是每次遍歷區間中的每一個數,記錄最大值和最小值。這樣可以正確求出正確答案,但是效率低下,時間複雜度高達 \(O(nq)\),無法透過本題。

之所以這樣做效率低下,是因為所有詢問區間可能有著大量的重疊,這些重疊部分被多次遍歷到,因此產生了大量的重複。如果可以透過預處理得到一些區間的最小值,再透過這些區間拼湊每一個詢問區間,就可以提高效率。

預處理字首和可以拼湊出任意區間的和,但是這個思路不能直接搬到最值查詢問題中。原因在於區間和可以從一個大區間中減去一部分小區間得到,而區間最值不行,所以只能用小區間去拼出大區間。如何選擇預處理的區間就成為關鍵,選擇的區間既要能夠拼出任意區間,數量少又不能太多,並且預處理和查詢都要高效。

可以預處理以每一個位置為開頭,長度為 \(2^0, 2^1, \cdots, 2^{\lfloor \log_2 n \rfloor}\) 的所有區間最值。下面以最大值為例,用 \(f_{i,j}\) 表示 \(h_i, h_{i+1}, \cdots, h_{i+2^j-2}, h_{i+2^j-1}\) 中的最大值,用遞推的方式計算所有的 \(f\),轉移為 \(f_{i,j} = \max (f_{i,j-1}, f_{i+2^{j-1}, j-1})\)。計算所有的 \(f\) 的過程為預處理,預處理的時間複雜度為 \(O(n \log n)\)

image

void init() {
	for (int i = 1; i <= n; i++) {
		f[i][0] = h[i];
	}
	for (int j = 1; (1 << j) <= n; j++) {
		for (int i = 1; i <= n - (1 << j) + 1; i++) {
			f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
		} 
	}
}

接下來解決查詢的問題,設需要查詢最大值的區間是 \([l,r]\)。記區間長度為 \(L\),則該區間可以拆分為 \(O(\log L)\) 個小區間。對 \(L\) 做二進位制拆分,從 \(l\) 開始向後跳,每次跳躍的量是一個 \(2\) 的冪,從而拼出整個區間。單詞查詢時間複雜度為 \(O(\log n)\)

int query(int l, int r) {
	int len = r - l + 1, ans = -INF, cur = l;
	for (int i = 0; (1 << i) <= len; i++) {
		if ((len >> i) & 1) {
			ans = max(ans, f[cur][i]);
			cur += (1 << i);
		} 
	}
	return ans;
}

更進一步,查詢區間最值時,區間合併的過程允許重疊,因此只需要找到兩個長度為 \(2^k\) 的區間合併得到 \([l,r]\)。令 \(k\) 為滿足 \(2^k \le r-l+1\) 的最大整數,區間 \([l, l+2^k-1]\) 和區間 \([r-2^k+1,r]\) 合併起來覆蓋了需要查詢的區間 \([l,r]\)

image

int query(int l, int r) {
	int k = log_2[r - l + 1]; // 可以預處理log_2的表
	return max(f[l][k], f[r - (1 << k) + 1][k]);
}
參考程式碼
#include <cstdio>
#include <algorithm>
using std::min;
using std::max;
const int N = 50005;
const int LOG = 16;
int h[N], f_min[N][LOG], f_max[N][LOG], log_2[N];
void init(int n) {
    log_2[1] = 0;
    for (int i = 2; i <= n; i++) log_2[i] = log_2[i >> 1] + 1; // 預處理對數表
    for (int i = 1; i <= n; i++) {
        f_min[i][0] = f_max[i][0] = h[i];
    }
    for (int j = 1; (1 << j) <= n; j++) {
        for (int i = 1; i <= n - (1 << j) + 1; i++) {
            f_min[i][j] = min(f_min[i][j - 1], f_min[i + (1 << (j - 1))][j - 1]);
            f_max[i][j] = max(f_max[i][j - 1], f_max[i + (1 << (j - 1))][j - 1]);
        }
    }
}
int query(int l, int r, int flag) { // flag為1時查詢最大值,為0時查詢最小值
    int k = log_2[r - l + 1];
    if (flag) return max(f_max[l][k], f_max[r - (1 << k) + 1][k]);
    else return min(f_min[l][k], f_min[r - (1 << k) + 1][k]);
}
int main()
{
    int n, q; scanf("%d%d", &n, &q);
    for (int i = 1; i <= n; i++) scanf("%d", &h[i]);
    init(n);
    for (int i = 1; i <= q; i++) {
        int a, b; scanf("%d%d", &a, &b);
        printf("%d\n", query(a, b, 1) - query(a, b, 0));
    }
    return 0;
}

Sparse Table 預處理部分的時間複雜度為 \(O(n \log n)\),查詢一次的時間複雜度為 \(O(1)\),總的時間複雜度為 \(O(n \log n)\)

Sparse Table 不僅可以求區間最大值和最小值,還可以處理符合結合律和冪等律(與自身做運算,結果仍是自身)的資訊查詢,如區間最大公約數、區間最小公倍數、區間按位或、區間按位與等。

相關文章