【算法】【学习笔记】稀疏表
稀疏表
稀疏表,又称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}\) 的小区间拼起来,推导公式如下:(例为求最大值)
前半段:从 \(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)——即内层循环(第二层循环)里变动的变量,对应二维数组的最后一个下标(即内层维度 / 列)性能更好,所以公式调转为:
2. 幂等性
像 max、min、gcd(最大公约数)这类运算,有一个特性叫幂等性(或可重叠性):
即:重复覆盖同一个元素不会影响最终的最值结果。
核心
当我们要查询区间 \([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]\)的数据。
因此,查询结果推导公式为:
同上的循环遍历问题,在代码中公式进行反转操作:
当建好表后,每次查询都只需要 \(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].微信读书版

浙公网安备 33010602011771号