【算法】【学习笔记】稀疏表

稀疏表

稀疏表,又称ST 表 (Sparse Table)。是一种专门用来高效解决区间最值查询 (Range Minimum/Maximum Query,RMQ)问题的数据结构。

核心优势:使用 \(O(N\log N)\) 的空间预处理后,单次查询时间只需要 \(O(1)\)

何时使用

  • 数组元素固定不会变;
  • 查询的运算必须满足可重复贡献性(幂等性);
  • 需要进行极多次区间最值(Min/Max)、最大公约数(GCD)查询;
  • 对查询延迟要求极高——需要 \(O(1)\) 的速度。

实现思路

核心思想

核心思想为两个:倍增(Doubling)和幂等性(Idempotence)。

1. 倍增预处理

定义一个二维数组 \(st[i][j]\)

\(st[i][j]\) 代表区间 \([i, i + 2^j - 1]\)(即从索引 i 开始、长度为 \(2^j\) 的区间)内的最值。即,每次 \(j\) 的增加都是增加 \(2^j\) 的长度。

因为任何长度都可以拆分成 \(2\) 的幂次相加,所以我们可以用动态规划的思想,把大区间拆成两个长度为 \(2^{j-1}\) 的小区间拼起来,推导公式如下:(例为求最大值)

\[st[i][j] = \max\left(st[i][j-1], \; st\left[i + 2^{j-1}\right][j-1]\right)\tag{1} \]

前半段:从 \(i\) 开始,长度为 \(2^{j-1}\),即 \(st[i][j-1]\)
后半段:从 \(i + 2^{j-1}\) 开始,长度为 \(2^{j-1}\),即 \(st[i + (1 << (j-1))][j-1]\)

所以 \(j\) 的取值范围为 \([0, \log_2 n]\)

注意:但是在代码中,循环填表时,外层循环是指数 \(j\),内层循环是下标 \(i\)。把 \(j\) 放在第一维(即\(st[j][i]\))可以保证内层循环访问连续的内存地址,极大地提升 CPU 缓存命中率(Cache Locality)——即内层循环(第二层循环)里变动的变量,对应二维数组的最后一个下标(即内层维度 / 列)性能更好,所以公式调转为:

\[st[j][i] = \max\left(st[j-1][i], \; st\left[j-1\right][i + 2^{j-1}]\right)\tag{2} \]

2. 幂等性

像 max、min、gcd(最大公约数)这类运算,有一个特性叫幂等性(或可重叠性):

\[\max(a, b, b, c) = \max(a, b, c)\tag{3} \]

即:重复覆盖同一个元素不会影响最终的最值结果。

核心

当我们要查询区间 \([L, R]\) 的最值时(区间长度 \(len = R - L + 1\)):
找到最大的整数 \(k\),使得 \(2^k \le len\)(即 \(k = \lfloor \log_2(len) \rfloor\));
我们用两个长度为 \(2^k\) 的区间拉头拉尾、互相覆盖:
左区间:\([L, \; L + 2^k - 1]\) \(\rightarrow\)st[L][k]
右区间:\([R - 2^k + 1, \; R]\) \(\rightarrow\)st[R - (1 << k) + 1][k]
由于幂等性,重复的数据不影响结果,将左右区间组合即可获得整个 \([L, R]\)的数据。

因此,查询结果推导公式为:

\[\text{Query}(L, R) = \max\left(st[L][k], \; st[R - 2^k + 1][k]\right)\tag{4} \]

同上的循环遍历问题,在代码中公式进行反转操作:

\[\text{Query}(L, R) = \max\left(st[k][L], \; st[k][R - 2^k + 1]\right)\tag{5} \]

当建好表后,每次查询都只需要 \(O(1)\) 的时间复杂度。

具体实现

以求最大值为例

class SparseTable {
    private st: number[][];      // st表
    private logTable: number[];  // log2数组

    constructor(nums: number[]) {
        const n = nums.length;
        const K = Math.floor(Math.log2(n)) + 1;

        // 1. 预处理 log2 数组(方便 O(1) 取 log)
        this.logTable = new Array(n + 1).fill(0);
        for (let i = 2; i <= n; i++) {
            this.logTable[i] = this.logTable[i >> 1] + 1;
        }

        // 2. 初始化 ST 表
        this.st = Array.from({ length: K }, () => new Array(n).fill(0));
        for (let i = 0; i < n; i++) {
            this.st[0][i] = nums[i]; // 长度为 2^0 = 1 的区间,就是元素本身
        }

        // 3. 动态规划填表 O(N log N)
        for (let j = 1; j < K; j++) {
            const len = 1 << (j - 1);  // 2^(j-1)
            for (let i = 0; i + (1 << j) <= n; i++) {
                this.st[j][i] = Math.max(this.st[j - 1][i], this.st[j - 1][i + len]);  // 此处用公式(2)
            }
        }
    }

    // O(1) 查询 [L, R] 区间最大值
    public query(L: number, R: number): number {
        const len = R - L + 1;
        const k = this.logTable[len];
        return Math.max(this.st[k][L], this.st[k][R - (1 << k) + 1]);  // 此处用公式(5)
    }
}

引用

[1] 陈小玉.算法训练营:提高篇(全彩版)[M].微信读书版

posted @ 2026-07-22 15:51  SEHOD  阅读(0)  评论(0)    收藏  举报