線段樹 (Segment Tree)

線段樹是一種專門拿來解決 RMQ (Range Minimum/Maximum Query) 問題的高效資料結構,常見的操作有兩種:區間查詢、單點更新。它高效的地方在於,除了每一層儲存自己區間的答案之外,它的上一層還會儲存下一層的每兩個相鄰區間合併起來形成的一個新區間的答案,如此一來這棵樹持續往上長到最後的最上層就會只剩下一個儲存全域答案的節點而已,這樣的話對於一道有 $n$ 個節點的問題,這棵樹最高也就是 $\mathcal{O}(\log_{2}n)$ 層,並總共花費至多 $4n-1 = \mathcal{O}(n)$ 的空間複雜度 [1]。對於區間查詢而言,每次由上而下的動作至多只會跑 $\mathcal{O}(\log_{2}n)$ 層,又每一層最多遇到 4 個節點 [2],於是時間複雜度仍然為 $\mathcal{O}(\log_{2}n)$;對於單點更新而言,……。

實作的部分:……

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int arr[500001], N;
int nodes[2000001]; // 每個節點都儲存自己區間的答案,並且節點號碼從 1 開始,才能遵循 LeftChild: 2 倍、RightChild: 2 倍 + 1 原則
inline int left(int p) { return p << 1; }
inline int right(int p) { return left(p) + 1; }
int build(int p, int l, int r) { // 求出區間 [l-r] 的答案並儲存進號碼 p 的節點
    // assert(l <= r); // 從中間剖分的過程中,左索引值不可能超過右索引值
    if (l == r) return nodes[p] = arr[l]; // 區間為單點元素時只得自行計算出答案
    int m = (l + r) >> 1; // 取中點
    auto a1 = build(left(p), l, m); // 求出區間 [l-m] 的答案並儲存進號碼 2p 的節點
    auto a2 = build(right(p), m+1, r); // 求出區間 [(m+1)-r] 的答案並儲存進號碼 2p + 1 的節點
    return nodes[p] = max(a1, a2); // 合併左右子樹的兩個答案以求出自己的答案,並存進節點中
}
int L, R; // 用於 recursive 和 query,表示欲查詢區間為 [L-R]
int recursive(int p, int l, int r) { // assert(l <= r); // 給定過程中已經有答案的節點區間 [l-r],最後會回傳此區間和欲查詢區間 [L-R] 交集之後的答案,注意這三個數字本應與 build 過程中所設定的一致
    if (r < L || R < l) return -INT_MAX; // 如果節點區間與欲查詢區間沒有重疊,則希望此答案為單位元素 (做運算之後不影響其他區間的答案)
    if (L <= l && r <= R) return nodes[p]; // 如果節點區間已經被完整包覆在欲查詢區間之內,則直接貢獻答案 (停止繼續遞迴)
    // 如果節點區間和欲查詢區間僅部分重疊,則把自己這個節點區間再切成兩半,分別繼續做 recursive
    int m = (l + r) >> 1; // 取中點,注意到下面的 left(p), l, m 和 right(p), m+1, r 的 pattern 會與 build 的過程完全相同
    return max(recursive(left(p), l, m), recursive(right(p), m+1, r));
}
int query(int l, int r) {
    L = l; R = r; // assert(L <= R); // 欲查詢區間 [L-R] 在計算過程中不會改變
    return recursive(1, 1, N); // 給定大區間 [1-N] 內所有子區間的已知資料,如何求出恰為 [L-R] 區間的答案?
}

int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); // IO 優化
    cin >> N;
    for (int i=1; i<=N; i++) cin >> arr[i];
    build(1, 1, N);
    int M; cin >> M;
    for (int i=0; i<M; i++) {
        int a, b; cin >> a >> b;
        if (a > b) swap(a, b);
        cout << query(a, b) << endl;
    }
    return 0;
}

參考資料:
[1] https://hackmd.io/@wiwiho/CPN-segment-tree#%E5%84%B2%E5%AD%98
[2] https://stackoverflow.com/a/59127491/11550178
[3] http://wiki.csie.ncku.edu.tw/acm/course/Segment_Tree
[4] https://cp-algorithms.com/data_structures/segment_tree.html