[数据结构]ST表(markdown重制版)
ST表
一、适用情况
ST 表 能在O(1)O(1)O(1)的时间内查询 满足可重复贡献问题(幂等性)和结合律的区间信息
如:区间最值,区间GCD,区间LCM,区间按位或,区间按位与 等
st[i][j]
表示起点为i
,区间长度为2j2^j2j的gcd
或者max
等
也就是a[i,i+(1<<j)-1]
内的gcd
二、如何递推?
首先要初始化
遍历每个i
将st[i][0]
赋值成a[i]
,因为j==0
表示区间长度为202^020 即为1
也就是起点所对应的元素本身
在两个相邻的区间内取max
或gcd
这两个区间的起点相邻 区间长度相同
都是2j−12^{j-1}2j−1 区间合并刚好就是要求的区间
三、如何查询?
query(L, R)
的具体步骤:
-
计算区间的长度:
首先,计算区间的长度:
len = R - L + 1
-
找到最大可以覆盖该区间长度的 2k2^k2k:
对于给定区间的长度,我们需要找到一个最大的k
,使得2^k
小于等于该长度len
。
这个k
可以通过 log2(len)log_2(len)log2(len)来计算,因为log2(len)log_2(len)log2(len)给出的是最大的k
,使得 2k≤len2^k \le len2k≤len,log2(len)log_2(len)log2(len)用int
接收 后面的小数就被截断
所以一个2k2^k2k无法完全覆盖len
,所以将目标区间拆成两个部分(有重叠) -
将区间
[L, R]
分为两个子区间:
通过选定的k
值,区间[L, R]
被分成两部分:
计算int k=log(r-l+1);
- 第一个子区间:从
L
到L + 2^k - 1
(即,长度为2^k
) - 第二个子区间:从
R - 2^k + 1
到R
(也是长度为2^k
)
子区间重叠对于max, gcd 等操作毫无影响,这就是st表巧妙的一点
通过k 将[l,r]分成两个可能有重叠的子区间 重叠并不会造成影响
四、代码实现
1. 模板
#include <iostream>
#include <cmath>
using namespace std;const int MAX_N = 1000; // 假设最大元素个数为1000
const int MAX_LOG = 10; // 假设 n 最多为 2^10 = 1024int arr[MAX_N]; // 存储原始数组
int st[MAX_N][MAX_LOG]; // ST表,st[i][j] 表示区间 [i, i + 2^j - 1] 的最小值
// 构建 ST 表
void build(int n) {for (int i = 0; i < n; i++) {st[i][0] = arr[i];}for (int j = 1; (1 << j) <= n; j++)for (int i = 0; i + (1 << j) - 1 < n; i++)st[i][j] = min(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]);
}int query(int L, int R) {int len = R - L + 1;int k = __lg(len);return min(st[L][k], st[R - (1 << k) + 1][k]);
}int main() {int n;cin >> n;for (int i = 0; i < n; i++) cin >> arr[i];build(n);int L, R;cin >> L >> R;cout << "区间 [" << L << ", " << R << "] 的最小值为: " << query(L, R) << endl;
}
1.__lg()
取以2为底的对数 向下取整 即 n 在二进制中的最高位 1 的位置(从 0 开始计数)时间复杂度O(1)O(1)O(1)
2.build中
外层循环 先遍历第二维j
就是按区间长度从小到大遍历 (1<<j)<=n
内层循环遍历起点i 循环结束条件是区间终点不越界 也就是i+(1<<j)-1<n
要记得区间终点-1 计算起点+1
2. Codeforces Round 991 (Div. 3)F题
const int N = 2e5 + 10;
int a[N], n, q, st[N][20];
// 最大的log(n),最大n=2e5所以log(2e5)大约为 18,取 20 以保证覆盖范围
void build() {for (int i = 0; i < n - 1; i++) st[i][0] = abs(a[i] - a[i + 1]);for (int j = 1; (1 << j) <= n; j++)for (int i = 0; i + (1 << j) - 1 < n - 1; i++)st[i][j] = gcd(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]);
}
int query(int l, int r) {int k = __lg(r - l + 1);return gcd(st[l][k], st[r - (1 << k) + 1][k]);
}
void solve() {cin >> n >> q;for (int i = 0; i < n; i++) cin >> a[i];// memset(st,1,sizeof st);for (int i = 0; i < n - 1; i++)for (int j = 0; j < 20; j++) st[i][j] = 1;build();while (q--) {int l, r;cin >> l >> r;if (l == r) {cout << 0 << ' ';continue;}l--, r -= 2;cout << query(l, r) << ' ';}cout << endl;
}
1.每个数mod m同余 相当于每个数的差都是m的倍数 只需要求区间的gcd就行 所以用st表
2.注意LOGN取20就行
3.不要用memset
初始化 比两层循环初始化为1慢了20倍 直接超时了 memset
不能填充1 它实际填充的不是 1,而是 0x01010101 = 16843009(对 int 来说)
4.l--,r-=2
首先l, r
都-1 是因为题目给的是1-based
我们把数组改成了0-based
其次 r再-1 是因为差分数组之后 原数组大小-1 查询的右区间也-1 找个例子模拟一下就行