前言
最近学习了ST表,所以写下这篇文章来记录一下,如有理解不到位的,欢迎大佬们在评论区留言
定义
ST是Sparse table的缩写,所以ST表是稀疏表;ST表本质是利用分治和倍增思想来求解的表,实战中通过预处理数据来得到一张表,然后通过查表得到结果;预处理可以做到O(nlogn),查询为O(1)
原理
之前说到,ST表就是用分治和倍增来求解得到的表,可以先看看洛谷这道题的题干https://www.luogu.com.cn/problem/P3865
分治和倍增体现在哪里呢?其实倍增体现在DP定义中,分治体现在递推公式中,ST表代码是利用了DP来进行预处理的
对于这道题,定义dp[i][j]表示左端点为i右端点为i+(2^j)-1的区间最大值
这里搬一下大佬的图
仔细观察区间就会发现[i,i+(2^j)-1]的区间长度正好是2的j次方,并且图示正好分一半,也就是一个区间分成两部分,每个部分都是2的j-1次方
所以可以这么理解,想求整个区间的最值,那么需要先求分开的两个区间的最值进行比较
这样就可以知道dp[i,j] = max(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);
既然定义和递推公式已经知道了,那么如何初始化和遍历呢?
从递推公式就可以知道,必须保证j>=1,也就是要求j=0时dp[i][0]等于什么;还记得定义是什么吗?dp[i][0]就表示为左端点i,右端点i+2^0-1区间的最大值。意思已经很明显了,端点本身这个数就是最值了;所以初始化时需要将dp[i][0]全部初始化为自身这个数
那么遍历又如何?是先i后j还是先j后i?
首先需要明确的是,j是与倍增直接相关的,所以必然有i + (1 << j) - 1 <= n;也就是说倍增不能够超出总的数字个数,但在代码中的实际含义应该是和范围有关,所以应该这么理解:i倍增后的右区间不能超过n,所以这个式子存在于i层,j层判断则是根据实际的数据规模进行判断,不难发现,必须要先遍历j再遍历i,否则上述式子没办法取值
对于询问应该怎么做?询问能做到O(1)必不可少就是查表
由于打表的时候是利用倍增思想打表,那么查表的时候自然也是要利用倍增的思想查表
所以先求log2(区间数字个数),再查表,查表时要覆盖左端点和右端点,所以分别以左端点和右端点开始做倍增,这样就一定能查到最值,下面这张图应该很清楚了,还是大佬的图
可以算一下,右区间-左区间+1就是闭区间的数字个数,比如:[0,5]的数字个数是5-0+1=6个
除开ST表的求解外,本题的数据非常严格,要求能预处理的都要预处理,所以还要设置一个对数数组来预处理,防止重复计算对数来优化询问时间
最后附上代码:
#include<cstdio>
#include<iostream>
using namespace std;
const int N = 100001;
int i, j, m, n, l, r, lg[N], dp[N][22]; //lg代表对数数组,存放处理的对数;dp[i][j]表示左闭区间i开始的2^j个数的最大值
int que(int x, int y)
{
int z = lg[y - x + 1];
return max(dp[x][z], dp[y - (1 << z) + 1][z]);
}
int main()
{
cin >> n >> m;
lg[0] = -1;
for (i = 1; i <= n; ++i)
{
scanf("%d", &dp[i][0]); //sacnf性能更高
lg[i] = lg[i >> 1] + 1; //预处理
}
for (j = 1; j < 22; ++j)
for (i = 1; i + (1 << j) - 1 <= n; ++i)
dp[i][j] = max(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);
for (i = 1; i <= m; ++i)
{
scanf("%d%d", &l, &r);
printf("%d\n", que(l, r));
}
return 0;
}
总结
ST表类问题的解决方法就是利用倍增的思想进行打表,查询时直接查表,注意数据规模就基本可以得到解决