ST表
\(ST\) 表
引入:
\(ST\) 表是用于解决可重复贡献问题的数据结构。
可重复贡献问题 是指对于运算 \(opt\) ,满足 \(x opt x=x\) ,则对应的区间询问就是一个可重复贡献问题。
例如,最大值有 \(max(x,x)=x\) ,\(gcd\) 有 \(gcd(x,x)=x\) ,所以 \(RMQ\) 和区间 \(GCD\) 就是一个可重复贡献问题。
像区间和就不具有这个性质,如果求区间和的时候采用的预处理区间重叠了,则会导致重叠部分被计算两次.
另外,\(opt\) 还必须满足结合律才能使用 \(ST\) 表求解。
例题:
\(ST\) 表基于 倍增 思想,可以做到 \(O(nlogn)\) 预处理, \(O(1)\) 回答每个询问。但是不支持修改操作。
基于倍增思想,我们考虑如何求出区间最大值。可以发现,如果按照一般的倍增流程,每次跳 \(2^i\) 步的话,询问时的复杂度仍旧是 \(O(logn)\) ,并没有比线段树更优,反而预处理一步还比线段树慢。
我们发现 \(max(x,x)=x\) ,也就是说,区间最大值是一个具有“可重复贡献”性质的问题。即使用来求解的预处理区间有重叠部分,只要这些区间的并是所求的区间,最终计算出的答案就是正确的。
如果手动模拟一下,可以发现我们能使用至多两个预处理过的区间来覆盖询问区间,也就是说询问时的时间复杂度可以被降至 \(O(1)\) ,在处理有大量询问的题目时十分有效。
具体实现如下:
令 \(f(i,j)\) 表示区间 \([i,i+2^{j−1}]\) 的最大值。
显然 \(f(i,0)=a_i\) 。
根据定义式,第二维就相当于倍增的时候“跳了 \(2^{j−1}\) 步”,依据倍增的思路,写出状态转移方程:
以上就是预处理部分。而对于查询,可以简单实现如下:
对于每个询问 \([l,r]\) ,我们把它分成两部分: \(f[l,l+2^{s}]\) 与 \(f[r−2^{s}+1,r]\)
其中 \(s=⌊log_2(r−l+1)⌋\)
由于最大值是“可重复贡献问题”,重叠并不会对区间最大值产生影响。又因为这两个区间完全覆盖了 \([l,r]\) ,可以保证答案的正确性。
模板代码:
#include<bits/stdc++.h>
using namespace std;
int f[100005][20];
int n,m;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&f[i][0]);
for(int j=1;j<=floor(log2(n));j++)
for(int i=1;i<=n+1-(1<<j);i++)
f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
for (int i=1;i<=m;i++){
int l,r;
scanf("%d%d",&l,&r);
int k=floor(log2(r-l+1));
printf("%d\n",max(f[l][k],f[r+1-(1<<k)][k]));
}
return 0;
}
小结:
稀疏表 \((SparseTable)\) 算法是 \(O(nlogn)−O(1)\) 的,对于查询很多大的情况下比较好。
\(ST\) 算法预处理:用 \(dp[i,j]\) 表示从 \(i\) 开始的,长度为 \(2^j\) 的区间的 \(RMQ\) ,则有递推式
即用两个相邻的长度为 \(2^{j−1}\) 的块,更新长度为 \(2^{j}\) 的块。因此,预处理时间复杂度为 \(O(nlogn)\)。
这个算法记录了所有长度形如 \(2^k\) 的所有询问的结果。
从这里可以看出,稀疏表算法的空间复杂度为 \(O(nlogn)\)
扩展:
除 \(RMQ\) 以外,还有其它的“可重复贡献问题”。例如“区间按位和”、“区间按位或”、“区间 \(GCD\)”,\(ST\) 表都能高效地解决。
需要注意的是,对于“区间 \(GCD\)”,\(ST\) 表的查询复杂度并没有比线段树更优(令值域为 \(w\) ,\(ST\) 表的查询复杂度为 \(Θ(logw)\) ,而线段树为 \(Θ(logn+logw)\) ,且值域一般是大于 \(n\) 的),但是 \(ST\) 表的预处理复杂度也没有比线段树更劣,而编程复杂度方面 \(ST\) 表比线段树简单很多。