引入
ST表这种数据结构可以用来解决可重复贡献问题
这里还不得不提一下什么是可重复贡献问题。
前置知识——可重复贡献问题
可重复贡献问题是对于运算 o p t opt opt,运算的性质满足 x o p t x = x x\;opt\;x = x xoptx=x,则对应的区间询问就是一个可重复的贡献问题,例如:最大值满足 m a x ( x , x ) = x max(x,x)=x max(x,x)=x,最大公因数满足 g c d ( x , x ) = x gcd(x,x)=x gcd(x,x)=x,因此 R M Q RMQ RMQ问题和 G C D GCD GCD问题就是一个可重复贡献的问题,但是例如区间和就不满足这个性质,因为在求解区间和的过程中采用的预处理区间会发生重叠,导致重叠部分被重复计算,因此对于 o p t opt opt操作还需要满足结合率才能够使用 S T ST ST表进行求解。
算法思想
我们先来看一个例子:
给定
n
n
n个数,有
m
m
m个询问,对于每个询问,你需要回答区间
[
l
,
r
]
[l,r]
[l,r]中的最大值。
暴力的算法显然是
O
(
n
2
)
O(n^2)
O(n2)的,这个算法的效率比较低下。所以我们考虑优化。
这个时候我们就可以引出 S T ST ST算法了:
S T ST ST表基于倍增思想,可以做到 O ( n l o g n ) O(nlogn) O(nlogn)预处理, O ( 1 ) O(1) O(1)回答每个询问。但是不支持修改操作。所以ST表是一种离线的数据结构。
基于倍增思想,我们考虑如何求出区间最值。可以发现,如果按照一般的倍增流程,每次跳 2 i 2^i 2i步的话,询问时的复杂度仍旧是 O ( l o g n ) O(logn) O(logn) ,并没有比线段树更优,反而预处理一步还比线段树慢。
我们发现,区间最大值是一个具有“可重复贡献”性质的问题。即使用来求解的预处理区间有重叠部分,只要这些区间的并是所求的区间,最终计算出的答案就是正确的。
如果手动模拟一下,可以发现我们能使用至多两个预处理过的区间来覆盖询问区间,也就是说询问时的时间复杂度可以被降至 ,在处理有大量询问的题目时十分有效。
以最大值为例,设 f [ i ] [ j ] f[i][j] f[i][j]表示整个数列 A A A中下标在子区间 [ i , i + 2 j − 1 ] [i,i+2^j-1] [i,i+2j−1]里的数的最大值,也就是从 i i i开始的 2 j 2^j 2j个数的最大值。递推边界显然是 f [ i ] [ 0 ] = A [ i ] f[i][0]=A[i] f[i][0]=A[i],即数列 A A A在区间 [ i , i ] [i,i] [i,i]里的最大值。
预处理
在递推时,我们把子区间的长度成倍增加,于是我们就能轻轻松松得到下面的这个递推表达式:
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
]
[
j
−
1
]
,
f
[
i
+
2
j
−
1
]
[
j
−
1
]
)
f[i][j]=max(f[i][j-1],f[i+2^{j-1}][j-1])
f[i][j]=max(f[i][j−1],f[i+2j−1][j−1])
根据这个思路,我们就可以写出预处理的代码(代码中用到了对数的换底公式)注意代码中的循环顺序一定是先枚举倍增次数,才能确保递推的正确性。
inline void prework(){
for(int i=1;i<=n;i++)f[i][0]=a[i];
int t=log(n)/log(2)+1;
for(int j=1;j<t;j++){
for(int i=1;i+(1<<j)-1<=n;i++){
f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
}
}
}
查询
当查询任意区间
[
l
,
r
]
[l,r]
[l,r]的最大值时,我们先计算出一个
k
k
k,满足:
2
k
≤
r
−
l
+
1
<
2
k
+
1
2^k \le r-l+1 \lt 2^{k+1}
2k≤r−l+1<2k+1
也就是
2
2
2的
k
k
k次幂小于区间长度的前提下的最大的
k
k
k。我们对上诉不等式进行求解:
先看左边:
2
k
≤
r
−
l
+
1
2^k \le r-l+1
2k≤r−l+1
两边同时取
l
o
g
2
log_2
log2为底的对数:
k
≤
l
o
g
2
(
r
−
l
+
1
)
k\le log_2(r-l+1)
k≤log2(r−l+1)
进行换底公式(C++中
l
o
g
log
log的底数为自然对数
e
e
e)
k
≤
l
o
g
(
r
−
l
+
1
)
l
o
g
2
k\le \frac{log(r-l+1)}{log2}
k≤log2log(r−l+1)
再看右边的式子,简单化简:
k
>
l
o
g
(
r
−
l
+
1
)
l
o
g
2
−
1
k\gt \frac{log(r-l+1)}{log2}-1
k>log2log(r−l+1)−1
由此可见,最大的
k
k
k就是
k
k
k的上界,因此区间
[
l
,
r
]
[l,r]
[l,r]之间的最大值就是:
m
a
x
(
f
[
l
]
[
k
]
,
f
[
r
−
(
1
<
<
k
)
+
1
]
[
k
]
)
max(f[l][k],f[r-(1<<k)+1][k])
max(f[l][k],f[r−(1<<k)+1][k])
inline int query(int l,int r){
int k=log(r-l+1)/log(2);
return max(f[l][k],f[r-(1<<k)+1][k])
}
这下就做到了 O ( 1 ) O(1) O(1)查询了。