ST表(RMQ)算法详解

499 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

ST的定义:

就是一个用来解决rmq(区间最值)问题的算法。

相关性质:

1.缺点:不支持修改操作。 2.时间复杂度: 预处理(O( n*log(n) ) 查询:(O(1));

具体操作:

1.首先建立f[i][j] 首先定义f[i,j]为以第i个数为起点,长度为2^j的一段区间中的最大值 2.预处理 根据状态方程预处理整个过程 3.查询。 //预处理复杂度同为O(nlogn),查询时间上,ST表为O(1),线段树为O(logn)

状态表达式: 一、什么是ST表 1.可重复贡献问题 可重复贡献问题是对于运算o p t optopt,运算的性质满足x o p t x = x x\ opt\ x\ =\ xx opt x = x,则对应的区间询问就是一个可重复的贡献问题,例如:最大值满足m a x ( x , x ) = x max(x, x) = xmax(x,x)=x,最大公因数满足g c d ( x , x ) = x gcd(x, x) = xgcd(x,x)=x,因此RMQ问题和GCD问题就是一个可重复贡献的问题,但是例如区间和就不满足这个性质,因为在求解区间和的过程中采用的预处理区间会发生重叠,导致重叠部分被重复计算,因此对于o p t optopt操作还需要满足集合率才能够使用ST表进行求解。

2.ST表简介 ST表是一种基于倍增思想,用于解决可重复贡献问题的数据结构。

ST表应用最广泛的领域便是解决RMQ问题:给定n nn个数,m mm个询问,对于每个询问,需要回答区间[ l , r ] [l, r][l,r]中的最大值或最小值(可以采用两个数组同时进行处理)。

基于倍增的思想,ST表可以实现O ( n l o g n ) O(n log n)O(nlogn)下进行预处理,并在0 ( 1 ) 0(1)0(1)时间内回答每个询问。如果仅仅进行区间最值查询,ST表的效率完全吊打线段树;但是,相比于线段树,ST表并不支持修改操作,无论是单点修改还是区间修改都不支持。

二、ST表的原理和实现 我们在简介中提到,ST表基于倍增的思想,但并不是朴素的倍增方式:对于朴素的倍增方式,我们可以发现查询的过程中,我们仍然需要调2 i 2^i2 i 步,那么查询的复杂度显然不是0 ( 1 ) 0(1)0(1),也不存在比线段树更有的说法,反倒预处理不如线段树快。

❓::那么倍增的思想是如何应用的呢?

我们在简介前提到了重复贡献问题,显然对于区间的最小值/最大值都满足这个性质:m i n ( x , x ) = x , m a x ( x , x ) = x min(x,x) = x,\ max(x, x) = xmin(x,x)=x, max(x,x)=x。那么显然用来求解的预处理的区间有重叠部分,只要这些区间的并是所求区间,最终计算出的答案一定就是正确答案。

❓::具体是如何实现的?

我们定义f [ i ] [ j ] f[i][j]f[i][j]表示区间[ i , i + 2 j − 1 ] [i, i + 2^j - 1][i,i+2 j −1]的最大值,那么显然f [ i ] [ 0 ] = a i f[i][0] = a_if[i][0]=a 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+2 j−1 ,j−1))​

而对于询问[ l , r ] [l, r][l,r],我们将其分成两部分f [ l , l + 2 s − 1 ] f[l, l + 2^s - 1]f[l,l+2 s −1]和f [ r − 2 s + 1 , r ] f[r - 2^s + 1, r]f[r−2 s +1,r]​,即为两个子区间。由于RMQ属于可重复贡献问题,因此不必使两个区间不相交。

但显然,我们需要使第一个字区间的右端点尽可能的接近r rr​,那么不妨直接令l + 2 s − 1 = r l + 2^s - 1 = rl+2 s −1=r,那么可以得到s = log ⁡ 2 ( r − l + 1 ) s = \log_2{(r - l + 1)}s=log 2 ​ (r−l+1);同时,我们希望第二个区间的左端点尽可能地接近l ll​,也就是r − 2 s + 1 = l r - 2^s + 1 = lr−2 s +1=l,发现于上一个式子使完全相同的,因此只需要取s = log ⁡ 2 ( r − l + 1 ) s = \log_2{(r -l + 1)}s=log 2 ​ (r−l+1)即可。但显然这样的s ss​不是整数​,那么我们直接取向下取整即可,此时的s ss仍能保证两个子区间完全覆盖整个区间。

由于使用STL反复计算l o g loglog值容易卡l o g loglog,对于s ss我们只需预处理出可能用到的全部s ss​​值即可: { L o g n [ 1 ] = 0 , L o g n [ i ] = L o g n [ i 2 ] + 1. \left{ Logn[1]Logn[i]=0,=Logn[i2]+1. Logn[1]=0,Logn[i]=Logn[i2]+1. \right. ⎩ ⎨ ⎧ ​

Logn[1] Logn[i] ​

=0, =Logn[ 2 i ​ ]+1. ​

ST表的其他应用

除 RMQ 以外,还有其它的“可重复贡献问题”。例如“区间按位和”、“区间按位或”、“区间 GCD”,ST 表都能高效地解决。

需要注意的是,对于“区间 GCD”,ST 表的查询复杂度并没有比线段树更优(令值域为 w ww,ST 表的查询复杂度为 Θ ( log ⁡ w ) \Theta(\log w)Θ(logw),而线段树为 Θ ( log ⁡ n + log ⁡ w ) \Theta(\log n+\log w)Θ(logn+logw),且值域一般是大于 n nn 的),但是 ST 表的预处理复杂度也没有比线段树更劣,而编程复杂度方面 ST 表比线段树简单很多。

如果分析一下,“可重复贡献问题”一般都带有某种类似 RMQ 的成分。例如“区间按位与”就是每一位取最小值,而“区间 GCD”则是每一个质因数的指数取最小值。

要知道,
f[i][j]=max(f[i][j-1],f[i+(1<<(j-1)][j-1]);

f[i][j-1]表示:
闭区间[i , i+2^j-1]   长度为:2^j-1的最大值

f[i+(1<<(j-1)][j-1]表示:
闭区间[i+(1<<(j-1) , i+2^j-1]   长度为:2^j-1的最大值

查询操作同理

代码附上:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>

using namespace std;
const  int N=1e5*2+100,M=18;
int f[N][M];
int a[N];
int n,m;
void init()
{
    for(int j=0;j<M;j++)
    {
        for(int i=1;i + (1<<j) -1<=n;i++)
        {
            if(!j)
            {
                f[i][j]=a[i];
            }
            else 
            {
                f[i][j]=max(f[i][j-1],f[i + (1<<(j-1))][j-1]);
            }
        }
    }
}
int query(int l,int r)
{
    int len=r-l+1;
    int k=log(len)/log(2);
   return  max(f[l][k],f[r- (1<<k)+1][k]);
}
int main()
{
    
    cin>>n;
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    
    init();

    scanf("%d", &m);
    while (m -- )
    {
        int l, r;
        scanf("%d%d", &l, &r);
        printf("%d\n", query(l, r));
    }

    return 0;
}

总结: 利用倍增的思想 将比较好合并的性质 利用 长度比较大的二进制块 合并起来。