問題描述

如果只是要求區間總和,可以透過前綴和在 $O(n)$ 預處理後,$O(1)$ 查詢任意區間和。但這種方法無法應用在區間最大值或最小值的查詢上。
那麼,有沒有辦法可以經過預處理後,在 $O(1)$ 內查出任意區間極值呢?


Range Minimum/Maximum Query (RMQ)

這類問題被稱為 RMQ(Range Minimum/Maximum Query),即區間極小值或極大值查詢


Sparse Table(稀疏表)簡介

操作時間複雜度
建表(預處理)$O(n \log n)$
區間查詢$O(1)$

💡 適用於「靜態」陣列(不會修改),如區間極值查詢問題。


小知識:Sparse Table 是什麼?

Sparse Table 是一種適合靜態陣列的資料結構,可用來在常數時間查詢任意區間的最大或最小值。
它的原理是:將每個起點 $i$ 往後長度為 $2^k$ 的區間極值先計算出來並存表

例如:

  • sparse[i][0]:= A[i]
  • sparse[i][1]:= max(A[i], A[i+1])
  • sparse[i][2]:= max(A[i], A[i+1], A[i+2], A[i+3])
  • 依此類推…

建表步驟說明

Step 1️⃣:建立 log2 陣列

用來快速查詢 floor(log2(k)),避免重複計算。

for (int i = 2; i <= n; i++) {
    lg[i] = lg[i / 2] + 1;
}

eg. n = [0,5]

log2
0無意義
10
21
31
42
52

Step 2️⃣:初始化 sparse[i][0]

每個 sparse[i][0] 就是 A[i] 本身,代表長度為 $2^0 = 1$ 的區間。

for (int i = 0; i < n; i++) {
    sparse[i][0] = A[i];
}

Step 3️⃣:遞推建表

每個 sparse[i][j] 代表從 i 開始、長度為 $2^j$ 的區間極值。 由兩個 $2^{j-1}$ 的子區間組成: 左:sparse[i][j-1] 右:sparse[i + 2^{j-1}][j-1]

for (int j = 1; j <= lg[n]; j++) {
    for (int i = 0; i + (1 << j) <= n; i++) {
        sparse[i][j] = max(sparse[i][j - 1], sparse[i + (1 << (j - 1))][j - 1]);
    }
}

查詢任意區間最大值

我們希望查詢的是 2^k 長度的區間,那非常簡單直接查表 sparse[i][k] 即可。

查詢 [L, R] 區間的最大值,流程如下:

  1. 計算區間長度:len = R - L + 1
  2. 找出最大整數 $k$,使得 $2^k \leq len$:
k = floor(log2(len))
  1. 拆成兩段長度為 $2^k$ 的子區間:
  • 第一段:[L, L + 2^k - 1]
  • 第二段:[R - 2^k + 1, R]
  1. 最後取兩段的最大值:

💡即使這兩段有重疊也沒關係,因為我們只在意極值,不怕重複計算。

result = max(sparse[L][k], sparse[R - (1 << k) + 1][k]);

eg. [0,8]

  1. len = 8 - 0 + 1 = 9
  2. k = floor(log₂(9)) = 3
  3. 左段 [0, 0 + 2^3 - 1] = [0,7];右段 [8 - 2^3 + 1, 8] = [1,8]
  4. result = max(sparse[0][3], sparse[1][3]);

其餘應用

除了找最大最小值,還可以查 GCD/LCM,把 max() 換成 __gcd() 或 lcm() 而已。

要應用的操作要滿足結合律,因為我們是把不同區間的結果整合!

完整程式碼

#include <bits/stdc++.h>
using namespace std;
int main() {
    // 測資大的話要加
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int n, q;
    cin >> n;
    vector<int> A(n);
    for (auto& x : A) {
        cin >> x;
    }
    cin >> q;
    vector<int> lg(n + 1);
    lg[1] = 0;
    for (int i = 2; i <= n; i++) {
        lg[i] = lg[i / 2] + 1;
    }
    vector<vector<int>> sparse(n, vector<int>(lg[n] + 1));
    for (int i = 0; i < n; ++i) sparse[i][0] = A[i];

    for (int j = 1; j < lg[n] + 1; j++) {
        for (int i = 0; i + (1 << j) <= n; i++) {
            sparse[i][j] = max(sparse[i][j - 1], sparse[i + (1 << (j - 1))][j - 1]);
        }
    }

    while (q--) {
        int l, r;
        cin >> l >> r;
        int j = lg[r - l + 1];
        cout << max(sparse[l][j], sparse[r - (1 << j) + 1][j]) << '\n';
    }
}

練習題

d539. 區間 MAX

連結:https://zerojudge.tw/ShowProblem?problemid=d539

  • 這題一定要加 IO 優化!!!
  • 要特別注意 query a b,a 不一定是左邊,所以要先判斷
if(l > r) swap(l,r);
  • 注意是 1-based,用上面的模板必須
l--;
r--;
  • 也不一定要建 log2 陣列,畢竟我們大概只會用到 lg[n] + 1 跟查詢的長度
vector<int> lg(n + 1);
lg[1] = 0;
for (int i = 2; i <= n; i++) {
    lg[i] = lg[i / 2] + 1;
}
// 可以改成
int max_log = __lg(n) + 1;
int j = lg[r-l+1];
// 可以改成
int j = __lg(r - l + 1);

本題 AC code

  • 0.3s 61.4MB
#include <bits/stdc++.h>
using namespace std;
int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int n, q;
    cin >> n;
    vector<int> A(n);
    for (auto& x : A) {
        cin >> x;
    }
    cin >> q;
    int max_log = __lg(n) + 1;
    vector<vector<int>> sparse(n, vector<int>(max_log));
    for (int i = 0; i < n; ++i) sparse[i][0] = A[i];

    for (int j = 1; j < max_log; j++) {
        for (int i = 0; i + (1 << j) <= n; i++) {
            sparse[i][j] = max(sparse[i][j - 1], sparse[i + (1 << (j - 1))][j - 1]);
        }
    }

    while (q--) {
        int l, r;
        cin >> l >> r;
        if (l > r) {
            swap(l, r);
        }
        l--;
        r--;
        int j = __lg(r - l + 1);
        cout << max(sparse[l][j], sparse[r - (1 << j) + 1][j]) << '\n';
    }
}