資料結構——RMQ(ST表)問題

RainPPR發表於2024-07-07

資料結構——RMQ(ST表)問題

問題描述

對於序列 \(A[1\dots n]\),有 \(m\) 組詢問 \(\langle l,r\rangle\),求 \(\max_{i=l}^rA_i\)

我們下面使用 \(\mathcal O(A)\sim\mathcal O(B)\) 表示預處理 \(\mathcal O(A)\),單次詢問 \(\mathcal O(B)\) 的時間複雜度。

線段樹解法

時間複雜度:\(\mathcal O(n)\sim\mathcal O(\log n)\)

空間複雜度:\(\mathcal O(n)\)

#include <bits/stdc++.h>

using namespace std;

constexpr int N = 2e5 + 10;

int n, a[N];

#define ls(x) ((x) << 1)
#define rs(x) ((x) << 1 | 1)

int seg[N << 2];

void push_up(int k) {
    seg[k] = max(seg[ls(k)], seg[rs(k)]);
}

void build(int k = 1, int l = 1, int r = n) {
    if (l == r) return void(seg[k] = a[l]);
    int mid = l + r >> 1;
    build(ls(k), l, mid);
    build(rs(k), mid + 1, r);
    push_up(k);
}

int query(int p, int q, int k = 1, int l = 1, int r = n) {
    if (l >= p && r <= q) return seg[k];
    int mid = l + r >> 1;
    if (p > mid) return query(p, q, rs(k), mid + 1, r);
    if (q < mid + 1) return query(p, q, ls(k), l, mid);
    else return max(query(p, q, ls(k), l, mid), query(p, q, rs(k), mid + 1, r));
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr), cout.tie(nullptr);
    cin >> n;
    copy_n(istream_iterator<int>(cin), n, a + 1);
    build();
    int q, l, r; cin >> q;
    while (q--) cin >> l >> r, cout << query(l, r) << endl;
    return 0;
}

修改複雜度 \(\mathcal O(\log n)\)

ST 表

ST 表可以做到 \(\mathcal O(n\log n)\) 預處理,\(\mathcal O(1)\) 求出序列區間最大值。

按照最基礎的思想,設 \(f(i,j)\) 表示區間 \([i,j]\) 的最大值,考慮上述倍增思想。

重新設計狀態,

\(f(i,j)\) 表示區間 \([i,i+2^j-1]\) 的最大值,也就是從 \(i\) 開始的 \(2^j\) 個數。

考慮這樣子遞推的邊界,

  • 顯然 \(f(i,0)=a_i\)
  • 顯然 \(f(i,j)=\max\{f(i,j-1),f(i+2^{j-1},j-1)\}\)

這麼折半的預處理,可以做到 \(\mathcal O(n\log n)\) 的複雜度。

考慮查詢,如果我們按照樸素的思想去處理的話,也是 \(\mathcal O(n\log n)\) 的,但是

有一個很簡單的性質,\(\max\{x,x\}=x\),這意味著我們可以重複計算一個區間的最大值。

於是,我們可以把區間中一部分重複的區間跳過,直接去計算:

能覆蓋整個區間的兩個左右端點上的整個區間,就可以做到 \(\mathcal O(1)\)

除 RMQ 以外,還有其它的「可重複貢獻問題」。例如「區間按位與」、「區間按位或」等。

ST 表能較好的維護「可重複貢獻」的區間資訊(同時也應滿足結合律),時間複雜度較低。

可重複貢獻問題是指滿足 \(x\operatorname{opt} x=x\) 的運算對應的區間詢問。

例如,\(\max(x,x)=x\)\(\operatorname{gcd}(x,x)=x\),等等。

所以 RMQ 和區間 GCD 就是一個可重複貢獻問題,像區間和就不具有這個性質。

如果預處理區間重疊了,則會導致重疊部分被計算兩次,這是我們所不願意看到的。

#include <bits/stdc++.h>

using namespace std;

constexpr int N = 2e5 + 10;
constexpr int K = 20;

int n, a[N];

int st[N][K];

#define pow2(x) (1 << (x))

void build() {
    for (int i = 1; i <= n; ++i) st[i][0] = a[i];
    for (int k = 1; k < K; ++k) for (int i = 1; i + pow2(k) - 1 <= n; ++i) st[i][k] = max(st[i][k - 1], st[i + pow2(k - 1)][k - 1]);
}

int query(int p, int q) {
    int k = log2(q - p + 1);
    return max(st[p][k], st[q - pow2(k) + 1][k]);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr), cout.tie(nullptr);
    cin >> n;
    copy_n(istream_iterator<int>(cin), n, a + 1);
    build();
    int q, l, r; cin >> q;
    while (q--) cin >> l >> r, cout << query(l, r) << endl;
    return 0;
}

不支援修改(複雜度很差)。

相關文章