【算法基础】你见过ST表吗?它竟然这么强大

ST表(Sparse Table)

ST表是一种用于区间查询的数据结构。它上面大部分的区间查询都是 O ( l o g n ) O(logn) O(logn)的时间。但它在查询区间最大值最小值问题上非常有效,只需要 O ( 1 ) O(1) O(1)的时间。

这种数据结构唯一的缺点是只能用于不可变的数组,就是说不支持更新操作。如果数组中元素更新了,那整个数据结构都要重新构造。

观察

任何一个非负整数都可以唯一表示为一组递减的2的幂的和,这等价于一个数的二进制表达。例如, 13 = ( 1101 ) 2 = 8 + 4 + 1 13 = (1101)_2 = 8 + 4 + 1 13=(1101)2=8+4+1。对于一个数 x x x最多需要 ⌈ l o g 2 x ⌉ \lceil log_2x \rceil log2x项。

同样的,任何一个区间都可以被唯一的表示为一组长度为2的幂的不相交区间的并集。比如, [ 2 , 14 ] = [ 2 , 9 ] ∪ [ 10 , 13 ] ∪ [ 14 , 14 ] [2,14] = [2,9] \cup [10,13] \cup [14,14] [2,14]=[2,9][10,13][14,14]。其中 [ 2 , 14 ] [2,14] [2,14]区间长度为13,而组成它的三个区间长度分别为8,4,1。同理,区间的数量不超过 ⌈ l o g 2 ( 区间长度 ) ⌉ \lceil log_2(区间长度) \rceil log2(区间长度)⌉

ST表的关键就是提前计算出所有长度为2的幂的区间的查询。之后任何一个查询都可以被分解成这些已知的查询的和。

预处理

我们使用一个二位数组来保存预处理的数据。 s t [ i ] [ j ] st[i][j] st[i][j]保存长度为 2 i 2^i 2i并且左端点为 j j j,即区间为 [ j , j + 2 i − 1 ] [j,j+2^i-1] [j,j+2i1]查询。这个二维数组的大小是 ( K + 1 ) × M A X N (K+1) \times MAXN (K+1)×MAXN,其中 M A X N MAXN MAXN是区间的最大长度。 K K K需要满足 K > ⌈ l o g 2 M A X N ⌉ K> \lceil log_2 MAXN \rceil K>log2MAXN,这是因为 2 l o g 2 M A X N 2^{log_2MAXN} 2log2MAXN是我们需要预处理保存的最长的区间。对于元素数量小于等于1e7的问题, K = 25 K=25 K=25就足够了。

由于计算机缓存的机制的存在, M A X N MAXN MAXN这一维放到数组的第二个维度比较快。

int st[K + 1][MAXN];

因为区间 [ j , j + 2 i − 1 ] [j,j+2^i - 1] [j,j+2i1]可以完美地被分解为区间 [ j , j + 2 i − 1 − 1 ] [j,j+2^{i-1}-1] [j,j+2i11]和区间 [ j + 2 i − 1 , j + 2 i − 1 ] [j+2^{i-1},j+2^i-1] [j+2i1,j+2i1]的并。我们可以通过动态规划的方法来高效地生成预处理的表。

std::copy(array.begin(), array.end(), st[0]);

for (int i = 1; i <= K; i++)
    for (int j = 0; j + (1 << i) <= N; j++)
        st[i][j] = f(st[i - 1][j], st[i - 1][j + (1 << (i - 1))]);

函数 f f f取决于查询的类型,对于求和的查询就是求和函数,对于最小值的查询就是求最小值函数。

预处理的时间复杂度是 O ( N l o g N ) O(NlogN) O(NlogN)

区间求和

对于区间求和的查询,函数 f f f就是 f ( x , y ) = x + y f(x,y) = x + y f(x,y)=x+y。我们可以用下面的代码构造数据结构:

long long st[K + 1][MAXN];

std::copy(array.begin(), array.end(), st[0]);

for (int i = 1; i <= K; i++)
    for (int j = 0; j + (1 << i) <= N; j++)
        st[i][j] = st[i - 1][j] + st[i - 1][j + (1 << (i - 1))];

为了回答区间 [ L , R ] [L,R] [L,R]的区间查询,我们从大到小迭代2的幂,只要当前 2 i 2^i 2i小于等于区间的长度( R − L + 1 R-L+1 RL+1),我们就把区间 [ L , L + 2 i − 1 ] [L,L+2^i-1] [L,L+2i1]的结果加上,继续求区间 [ L + 2 i , R ] [L+2^i,R] [L+2i,R]的结果。

long long sum = 0;
for (int i = K; i >= 0; i--) {
    if ((1 << i) <= R - L + 1) {
        sum += st[i][L];
        L += 1 << i;
    }
}

区间求和的时间复杂度为 O ( K ) O(K) O(K),即 O ( l o g M A X N ) O(logMAXN) O(logMAXN)

区间最小值查询

区间最值查询就是ST表真正擅长的地方了(It shines in this type of query.)。当查询区间最小值的时候,区间的重叠对结果没有影响。因为相对于把区间分解成多个不相交的区间,我们可以把区间分解成两个长度为2的幂的两个区间,这两个区间可能重叠也可能不重叠。比如,我们可以把区间 [ 1 , 6 ] [1,6] [1,6]分解为区间 [ 1 , 4 ] [1,4] [1,4]和区间 [ 3 , 6 ] [3,6] [3,6]。很显然区间 [ 1 , 6 ] [1,6] [1,6]的最小值,就是区间 [ 1 , 4 ] [1,4] [1,4]和区间 [ 3 , 6 ] [3,6] [3,6]的最小值的最小值。所以我们可以通过下面的式子计算区间 [ L , R ] [L,R] [L,R]的最小值:
m i n ( s t [ i ] [ L ] , s t [ i ] [ R − 2 i + 1 ] ) 其中 i = l o g 2 ( R − L + 1 ) min(st[i][L], st[i][R-2^i + 1]) \quad \text{其中} i=log_2(R-L+1) min(st[i][L],st[i][R2i+1])其中i=log2(RL+1)
这个式子里还需要高效地计算 l o g 2 ( R − L + 1 ) log_2(R-L+1) log2(RL+1)的值,这个可以通过预处理计算出所有需要的log值:

int lg[MAXN+1];
lg[1] = 0;
for (int i = 2; i <= MAXN; i++)
    lg[i] = lg[i/2] + 1;

log值也可以在常数时间内动态的计算:

// C++20
#include <bit>
int log2_floor(unsigned long i) {
    return std::bit_width(i) - 1;
}

// pre C++20
int log2_floor(unsigned long long i) {
    return i ? __builtin_clzll(1) - __builtin_clzll(i) : -1;
}

压力测试表示使用lg数组计算会更慢一些,这是因为计算机缓存机制的存在。

查询区间 [ L , R ] [L,R] [L,R]结果的代码如下:

int i = lg[R - L + 1];
int minimum = min(st[i][L], st[i][R - (1 << i) + 1]);

区间最小值查询的时间复杂度为 O ( 1 ) O(1) O(1)

  • 17
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值