跳到主要内容

ST 表

ST 表也可以看做是一种区间 DP

不过它有自己的独特之处,所以和其他区间 DP 显得很不一样。

ST 表是一种用倍增法解决可重复贡献问题的(离线)(区间查询)算法,可以做到 O(nlog2n)O(n \log_2n) 预处理, O(1)O(1) 回答。

和树状数组、线段树等不同,ST 表查询很快,可以处理 1e9 级别的数据。也就是说,1e9 级别基本就是 ST 表了,线段树就别想了。

什么是“可重复贡献问题”?

定义 “ \circ ” 为一种二元运算,如果这种运算满足 xx=xx \circ x=x,就说求区间上若干个 xix_i\circ 是一个可重复贡献问题。

实例:区间最大/最小值(Range Maximun/Minnimun Query, RMQ)、GCD 等。(如 max{x,x}=x\max\{x,x\}=x

反例:区间和、区间积等。(如 x+xxx+x\not=x

正题

为了叙述方便,下面都以维护区间最大值为例讲解,其他的操作类似。

定义 ST[i][j]\text{ST}[i][j] 表示区间 [i,i+2j1][i,i+2^j-1] 上的最大值。即左端点为 ii ,包含 2j2^j 个数的区间。一般代码里面是这样开数组的:

const int MAXN = 1000;
int st[MAXN][10]; // 2^10 = 1024 > 1000

一般来说,应用 ST 表有两步:

  1. 预处理 ST 表,时间复杂度 O(nlog2n)O(n\log_2n)

  2. 离线处理区间查询(直接查表),时间复杂度 O(1)O(1)

预处理

通过递推来预处理。根据定义,容易得到递推式:

ST[i][j]=max{ST[i][j1],ST[i+2j1][j1]}\text{ST}[i][j]=\max\{\text{ST}[i][j-1],\text{ST}[i+2^j-1][j-1]\}

分析:

  • ST[i][j1]区间[i,i+2j11]\text{ST}[i][j-1]\longrightarrow区间[i,i+2^{j-1}-1]

  • ST[i+2j1][j1]区间[i+2j1,(i+2j1)+2j11] 即 区间[i+2j1,i+2j1]\text{ST}[i+2^{j-1}][j-1]\longrightarrow区间[i+2^j-1,(i+2^{j-1})+2^{j-1}-1]\space即\space区间[i+2^j-1,i+2^j-1]

  • 两个区间取并集,就是 ST[i][j]区间[i,i+2j1]\text{ST}[i][j]\longrightarrow区间[i,i+2^j-1]

这样,我们就可以由 ST[...][j1]\text{ST}[...][j-1] 递推到 ST[...][j]\text{ST}[...][j]

for (int i = 1; i <= n; i++) cin >> st[i][0]; // 直接输入到 st[i][0],省事
// 预处理
for (int j = 1; (1<<j) <= n; j++) {
// (1<<j) <= n 防止越界
// 外层是 j 循环
for (int i = 1; i+(1<<j)-1 <= n; i++) {
// 这里也要 i+(1<<j)-1 <= n 防止越界
st[i][j] = max(st[i][j-1], st[i+(1<<j-1)][j-1]); // 1<<j-1 就是 1<<(j-1)
}
}

查表

设我们要查询 [L,R][L,R] 上的区间最大值。

我们想要把 [L,R][L,R] 分为两个长度相等、首尾相连的小区间,然后通过求这两个小区间的最大值求得 [L,R][L,R] 的最大值。

假设两个小区间的长度都为 xx,区间 [L,R][L,R] 的长度 len=RL+1\text{len}=R-L+1。令 xx 为小于 len\text{len}22 的最高次幂,则有 xlen2xx\leq\text{len}\leq2x,可以保证一定覆盖 [L,R][L,R]。(可能会在中间重合)

所以我们想要的两个区间是:[L,L+x1][L,L+x-1][Rx+1,R][R-x+1,R]。两个区间的长度都是 xx

即:ST[L][log2x]\text{ST}[L][\log_2x]ST[Rx+1][log2x]\text{ST}[R-x+1][\log_2x]

实际编码时,我们先求出 k=log2lenk=\lfloor\log_2\text{len}\rfloor,于是有 2k=x2^k=x。我们用 kk 来计算,而不是 xx

cin >> L >> R;
int k = Log2[R-L+1];
cout << max(st[L][k], st[R-(1<<k)+1][k] << endl;

因为每次查询都要调用 log() 太慢了,我们选择自己预处理 Log2[]

// 预处理 Log2[]
Log2[1] = 0;
for (int i = 2; i <= n; i++) {
Log2[i] = Log2[i>>1] + 1;
}