在RMQ(區間最值)問題中,著名的ST演演算法就是倍增的產物。ST演演算法可以在 \(O(n \log n)\) 的時間複雜度能預處理後,以 \(O(1)\) 的複雜度線上回答區間 [l, r]
內的最值。
當然,ST表不支援動態修改,如果需要動態修改,線段樹是一種良好的解決方案,是 \(O(n)\) 的預處理時間複雜度,但是查詢需要 \(O(\log n)\) 的時間複雜度
那麼ST表中倍增的思想是如何體現的呢?
一個序列的子區間明顯有 \(n^2\) 個,根據倍增的思想,我們在這麼多個子區間中選擇一些長度為 \(2\) 的整數次冪的區間作為代表值。
設 \(st[i][j]\) 表示子區間 \([i, i+2^j)\) 裡最大的數
也可以表示為 \([i, i + 2^j -1 ]\),無論如何,其中有 \(2^j\) 個元素
下文中的 \(a\) 表示原序列
遞推邊界明顯是 \(st[i][0] = a[i]\)。
於是,根據成倍增長的長度,有了遞推公式
當詢問任意區間 \([l, r]\) 的最值時,我們先計算出一個最大的 \(k\) 滿足:\(2^k \le r - l + 1\),即需要不大於區間長度。那麼,由於二進位制劃分我們可以知道,這個最大的 k
一定滿足 \(2^{k+1}\ge r-l+1\),即我們只需要將兩個長度為 \(2^k\) 的區間合併即可。
又根據 max(a, a) = a
可以知道,重複計算區間是沒有任何問題的。
所以,在尋找最值的時候就有了以下公式:
那麼這裡給出一種參考程式碼
// 啊,寫這種預處理以2位底的對數的整數值的方式
// 我主要是為了將程式碼模組化,做到低耦合度
// 完全是可以分開來寫的
class Log2Factory {
private:
int lg2[N];
public:
void init(int n) {
for (int i = 2; i <= n; ++i) lg2[i] = lg2[i >> 1] + 1;
}
// 過載()運運算元
int operator() (const int &i) {
return lg2[i];
}
};
template<typename T>
class STable {
private:
typedef T(*OP_FUNC)(T, T);
Log2Factory Log2;
T f[N][17]; // maybe most of the times k=17 is ok, make sure 2^k greater than N;
OP_FUNC op;
public:
void setOp(OP_FUNC fc) {
op = fc;
}
void init(T *a, int n) {
for (int i = 1; i <= n; ++i)
f[i][0] = *(++a);
int t = Log2(n);
// f[i][k] is the interval of [i, i + 2^k - 1]
// so f[i][k] can equal to the op sum of [i, i^k - 1]
// let r = i^k - 1
// => f[r - (1^k) + 1][k] can equal to the op sum of [i][k]
for (int k = 1; k <= t; ++k) {
for (int i = 1; i + (1<<k) - 1 <= n; ++i)
f[i][k] = op(f[i][k-1], f[i + (1<<(k-1))][k-1]);
}
}
const T query(int l, int r) {
int k = Log2(r - l + 1);
return op(f[l][k], f[r - (1<<k) + 1][k]);
}
};
這……寫法很神奇,注意修改!
ST 演演算法不僅僅是可以求區間的最值的,只要是滿足靜態的,滿足區間加法的問題大多數情況都可以通過 ST 表實現。
那麼區間加法是什麼意思呢?
定義我們需要對數列的篩選函數為 op
,則需要 op
滿足以下性質
op(a, a) = a
,即重複參與運算不改變最終影響
op(a, b) = op(b, a)
,即滿足交換律
op(a, op(b, c)) = op(op(a, b), c)
,即滿足結合律
舉個例子,如果我們求區間是否有負數,可以將 op
設為如下邏輯:
bool op(bool a, bool b) {
return a | b;
}
相應的,初始化的方式也需要更改
if (a[i] < 0) st[i][0] = true;
else st[i][0] = false;
再舉一個例子,如果我們需要求區間是否全為偶數時,則初始化為
if (a[i] % 2 == 0) st[i][0] = true;
else st[i][0] = false;
操作 op
定義為
bool op(bool a, bool b) {
return a & b;
}
由此可見,其實ST演演算法可以做到的不僅僅是區間最值那麼普通的東西啊。
但是,由於
加法
不滿足性質一,所以,ST表通過這種方法並不能求得區間的所有滿足某種性質的元素的個數。但是,通過另外一種query
方式,我們可以做到這樣。
那麼這個部分我們將討論如何利用ST表做到上文例子中求區間偶數的個數。
同樣,由於我們可以通過二進位制劃分,所以可以將某一個區間長度轉化為多個長度為2的整數冪次方的子區間,並且可以保證這些區間不相互重疊。
所以我們可以利用這個處理 op(a, a) != a
的情況了。
其實這是借鑑了一點線段樹的思路
還不如直接用線段樹……
那麼可以寫出以下程式碼
int query(int l, int r) {
if (l == r) return st[l][0];
int k = log2(r - l + 1);
return op(st[l][k], query(l + (1<<k), r))
}
這樣就滿足了區間不重疊
或許會有一個問題,為什麼初始化的時候不需要修改?
其實不難發現,初始化的合併是不會有重複貢獻的情況的,即是每一次合併的區間是不會重疊的