ACM 学习篇 12:ST 表(稀疏表)

一、为什么学 ST 表

线段树可以在 O(log n) 时间内完成单点更新和区间查询,但如果数据是静态的(建好后不再修改),ST 表就是更优的选择:预处理 O(n log n),查询 O(1)。

ST 表的核心思想是倍增:预处理出所有长度为 2^k 的区间答案,查询时把 [l, r] 拆成两个重叠的 2^k 区间。由于很多运算满足幂等律(如 max、min、GCD),重叠不会影响结果。

二、核心原理

预处理:st[i][k]

st[i][k] 表示以 i 开头、长度为 2^k 的区间内的最值。

  • st[i][0] = a[i](长度为 1 的区间就是自己)
  • st[i][k] = op(st[i][k-1], st[i + 2^(k-1)][k-1])

状态转移把区间分成两半,左半部分是 [i, i+2^(k-1)-1],右半部分是 [i+2^(k-1), i+2^k-1]。

查询:RMQ(l, r)

设区间长度为 len = r - l + 1,取 k = floor(log2(len))。则:

  • 左区间:[l, l + 2^k - 1]
  • 右区间:[r - 2^k + 1, r]

这两个区间都覆盖了 [l, r],取最值即可。

适用范围

ST 表只适用于满足幂等律的操作,即 op(a, op(b, c)) = op(op(a, b), c),且 op(x, x) = x。

  • 最大值、最小值:✓
  • GCD(最大公约数):✓
  • 区间和:✗(不满足幂等律)

三、模板代码

区间最大值

class SparseTable {
private:
    vector> st;
    vector lg2;  // 整数对数,用于 O(1) 查询
    int n;

    // op 函数:这里用 max
    int op(int a, int b) { return max(a, b); }

public:
    SparseTable(const vector& a) {
        n = a.size() - 1;  // 假设 a 下标从 1 开始
        int K = log2(n) + 1;
        st.assign(K, vector(n + 1));

        // 预处理 k = 0
        for (int i = 1; i <= n; i++) st[0][i] = a[i];

        // 预处理 k > 0
        for (int k = 1; k < K; k++) {
            for (int i = 1; i + (1 << k) - 1 <= n; i++) {
                st[k][i] = op(st[k-1][i], st[k-1][i + (1 << (k-1))]);
            }
        }

        // 预处理对数
        lg2.resize(n + 1);
        lg2[1] = 0;
        for (int i = 2; i <= n; i++) lg2[i] = lg2[i/2] + 1;
    }

    // O(1) 查询
    int query(int l, int r) {
        int k = lg2[r - l + 1];
        return op(st[k][l], st[k][r - (1<

区间 GCD

class GcdSparseTable {
private:
    vector> st;
    vector lg2;
    int n;

public:
    GcdSparseTable(const vector& a) {
        n = a.size() - 1;
        int K = log2(n) + 1;
        st.assign(K, vector(n + 1));

        for (int i = 1; i <= n; i++) st[0][i] = a[i];
        for (int k = 1; k < K; k++) {
            for (int i = 1; i + (1 << k) - 1 <= n; i++) {
                st[k][i] = std::gcd(st[k-1][i], st[k-1][i + (1 << (k-1))]);
            }
        }

        lg2.resize(n + 1);
        lg2[1] = 0;
        for (int i = 2; i <= n; i++) lg2[i] = lg2[i/2] + 1;
    }

    int query(int l, int r) {
        int k = lg2[r - l + 1];
        return std::gcd(st[k][l], st[k][r - (1<

四、典型题目

RMQ 模板题

题意:给定数组,动态查询区间 [l, r] 的最大值或最小值。

思路:静态数组,直接用 ST 表。预处理 O(n log n),每次查询 O(1)。

LCA:最近公共祖先

两棵树做 LCA 时,可以预处理欧拉序和深度,用 ST 表维护欧拉序中相邻节点深度的最小值对应的节点编号。

  • 欧拉序:DFS 进入节点时记录一次,离开时再记录一次,形成长度为 2n-1 的序列。
  • 在欧拉序中,u 和 v 之间的区间就是从 u 往上到 LCA 再到 v 的路径。
  • 区间深度最小值对应的节点就是 LCA。
class LCA {
private:
    vector> st;   // 节点编号的 ST 表
    vector first, depth, euler;
    vector lg2;
    int timer = 0;

    void dfs(int u, int p, int d, const vector>& g) {
        first[u] = timer;
        euler.push_back(u);
        depth.push_back(d);
        timer++;

        for (int v : g[u]) {
            if (v == p) continue;
            dfs(v, u, d + 1, g);
            euler.push_back(u);
            depth.push_back(d);
            timer++;
        }
    }

    int queryMinDepth(int l, int r) {
        int k = lg2[r - l + 1];
        // 返回深度最小值对应的节点(需要另一个 ST 表存储节点)
        // 这里简化处理,实际需要维护 (depth, node) 的二元组
        return r;  // 占位
    }

public:
    LCA(int n, int root, const vector>& g) {
        first.assign(n + 1, 0);
        dfs(root, 0, 0, g);

        int m = euler.size();
        int K = log2(m) + 1;
        st.assign(K, vector(m));

        // 用另一个数组存储欧拉序,用 RMQ 找 LCA
    }

    int lca(int u, int v) {
        int l = first[u], r = first[v];
        if (l > r) swap(l, r);
        // 在欧拉序区间 [l, r] 中找深度最小的节点
        int k = lg2[r - l + 1];
        int minDepth = INT_MAX;
        int lcaNode = 0;
        // 实际需要二维 ST 或在 RMQ 中返回节点
        return lcaNode;
    }
};

离线区间最值

题意:给定数组 a 和 q 个查询 [l, r],求每个区间的最大值(静态)。

思路:用 ST 表 O(n log n) 预处理后,O(1) 回答每个查询。总复杂度 O(n log n + q)。

五、易错点

  • 数组下标:ST 表通常用 1-indexed,代码中要统一。
  • lg2 预处理:查询时用 lg2[len] 而不是 log2(len),避免浮点误差。
  • 空间:st 数组大小约为 n * log2(n),n = 10^5 时约 1.7M 个元素,可以接受。
  • ST 表不支持修改:如果需要单点更新,切换成线段树。
  • 类型选择:区间和不能用 ST 表,换成前缀和或线段树。

六、复盘

ST 表的特征是:静态数据 + 区间最值/GCD + 需要 O(1) 查询。见到这类约束,直接想 ST 表。

常见提示词:

  • "多组查询,查询互不影响"
  • "n 和 q 都很大(10^5),需要 O(1) 查询"
  • "数组不会修改"

学完 ST 表后,数据结构这一块已经覆盖了:

  • 栈/队列、单调栈/队列 → 线性结构
  • 堆/优先队列 → 完全二叉树结构
  • 哈希表 → 散列结构
  • 并查集 → 森林结构
  • 树状数组 → 二叉索引树
  • 线段树 → 区间树
  • ST 表 → 倍增稀疏表

下一块可以往 Trie 字典树走,也可以开始动态规划专题。Trie 是字符串高频考点,也是 AC 自动机和后缀自动机的基础。