稀疏表(ST表)
此文介绍一种数据结构——ST表(Sparse Table),以及如何使用这个数据结构解决可重复贡献问题。
可重复贡献问题
可重复贡献问题指的是,对于一种运算 o p t opt opt,满足 o p t ( x , x ) = x opt(x,x) = x opt(x,x)=x,并且 o p t opt opt可以参与区间运算,满足 ∀ l ≤ k ≤ r , o p t [ l , r ] = o p t ( o p t [ l , k ] , o p t [ k , r ] ) \forall l \leq k \leq r,opt \ [l,r] = opt(opt \ [l,k],opt \ [k,r]) ∀l≤k≤r,opt [l,r]=opt(opt [l,k],opt [k,r]), o p t opt opt还必须满足结合律。之后给定任意区间 [ l , r ] [l,r] [l,r],求解 o p t [ l , r ] opt \ [l,r] opt [l,r]的问题。
我们特化一下这个定义,例如,最大值有 max ( x , x ) = x \max(x,x) = x max(x,x)=x ,gcd 有 gcd ( x , x ) = x \gcd(x,x) = x gcd(x,x)=x ,所以 RMQ 和区间 GCD 就是一个可重复贡献问题。像区间和就不具有这个性质,如果求区间和的时候采用的预处理区间重叠了,则会导致重叠部分被计算两次,这是我们所不愿意看到的。另外, o p t opt opt还必须满足结合律才能使用 ST 表求解。
下文把RMQ问题作为例子讲解ST表。
ST表构建
我们定义 M a x ( i , e ) Max(i,e) Max(i,e)为区间 [ i , i + 2 e − 1 ] [i,i+2^{e} - 1] [i,i+2e−1]中的最大值,这个区间的长度是 2 e 2^{e} 2e,特别的 M a x ( i , 0 ) = a i Max(i,0) = a_{i} Max(i,0)=ai,其中 a i a_{i} ai表示数组中的第 i i i个元素(数组下标从1开始)。
之后,我们使用倍增的思想,对这个 M a x ( i , e ) Max(i,e) Max(i,e)数组进行倍增。即下式:
M a x ( i , e ) = max ( M a x ( i , e − 1 ) , M a x ( i + 2 e − 1 , e − 1 ) ) Max(i,e) = \max(Max(i,e-1),Max(i + 2^{e-1},e-1)) Max(i,e)=max(Max(i,e−1),Max(i+2e−1,e−1))
其中时间复杂度为
Θ
(
n
log
n
)
\Theta(n \log n)
Θ(nlogn)。
ST表查询
对于ST表的查询也很简单,只需要计算出查询区间的长度对应的 e e e,然后左端点查询一下,右端点查询一下即可,以保证覆盖全部的区间。
M a x ( l , r ) = max ( M a x ( l , k ) , M a x ( r − 2 k + 1 , k ) ) k = ⌊ log 2 ( r − l + 1 ) ⌋ Max(l,r) = \max(Max(l,k),Max(r - 2^{k} + 1,k)) \\ k = \left \lfloor \log_{2}(r - l + 1) \right \rfloor Max(l,r)=max(Max(l,k),Max(r−2k+1,k))k=⌊log2(r−l+1)⌋
其中时间复杂度为
Θ
(
1
)
\Theta(1)
Θ(1)。
对数优化
在计算对数 log 2 n \log_{2}n log2n的时候,我们可以利用倍增的思想构造对数表,即:
log 2 1 = 0 … log 2 n = log 2 ⌊ n 2 ⌋ + 1 \log_{2}1 = 0 \\ \ldots \\ \log_{2}n = \log_{2}\left \lfloor \frac{n}{2} \right \rfloor + 1 log21=0…log2n=log2⌊2n⌋+1
例题
#include <bits/stdc++.h>
using namespace std;
int MAX[1000010][18];
int LOG[1000010];
inline int read()
{
int x = 0, f = 1;
char ch = getchar();
while (!isdigit(ch))
{
if (ch == '-')
f = -1;
ch = getchar();
}
while (isdigit(ch))
{
x = x * 10 + ch - 48;
ch = getchar();
}
return x * f;
}
void buildST(int len)
{
for (int e = 1; e < 18; e++)
{
for (int i = 1; i + (1 << e) - 1 <= len; i++)
{
MAX[i][e] = max(MAX[i][e - 1], MAX[i + (1 << (e - 1))][e - 1]);
}
}
LOG[1] = 0;
for (int i = 2; i <= len; i++)
{
LOG[i] = LOG[i / 2] + 1;
}
}
int queryST(int l, int r)
{
int e = LOG[r - l + 1];
return max(MAX[l][e], MAX[r - (1 << e) + 1][e]);
}
int main()
{
int len = read(), m = read();
for (int i = 1; i <= len; i++)
MAX[i][0] = read();
buildST(len);
while (m--)
{
int l = read(), r = read();
printf("%d\n", queryST(l, r));
}
return 0;
}
逆向ST表
逆向ST表的思路是我们定义 M a x ( i , e ) Max(i,e) Max(i,e)为区间 [ i − 2 e + 1 , i ] [i-2^{e} + 1,i] [i−2e+1,i]中的最大值正好对应 i i i的逆向。因此,在这种情况下,在ST表的尾部插入一个节点是简单的,因为插入的这个节点不会影响前面节点的建立,并且只需要更新关于新节点的数组即可。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define FR freopen("in.txt", "r", stdin)
ll st[200005][18];
int LOG[200005];
int idx = 0;
void initLOG()
{
LOG[1] = 0;
for (int i = 2; i < 200005; i++)
{
LOG[i] = LOG[i / 2] + 1;
}
}
ll query(int L)
{
int sta = idx - L;
int ed = idx - 1;
int e = LOG[ed - sta + 1];
return max(st[ed][e], st[sta + (1 << e) - 1][e]);
}
void insert(ll val)
{
st[idx][0] = val;
for (int e = 1; idx - (1 << e) + 1 >= 0; e++)
{
st[idx][e] = max(st[idx][e - 1], st[idx - (1 << (e - 1))][e - 1]);
}
idx++;
}
int main()
{
initLOG();
int m;
ll p;
cin >> m >> p;
ll t = 0;
while (m--)
{
char op;
ll n;
cin >> op >> n;
if (op == 'A')
{
n = (n + t) % p;
insert(n);
}
else if (op == 'Q')
{
cout << (t = query(n)) << endl;
}
}
return 0;
}
关于GCD的ST表
此题应考虑差分,如果一个连续的子序列有相同的余数,那么他们的差分都应该是商的倍数,即连续的差分数组的GCD大于1。
求一个连续区间的GCD应考虑ST表,然后再加上尺取即可。