引入
直接通过一道经典的例题来引入st表以及倍增的概念。
落谷P3865 ST表
描述
给定一个长度为的数列,和次询问,求出每一次询问的区间内数字的最大值。
输入格式
第一行包含两个整数,分别表示数列的长度和询问的个数。
第二行包含个整数(记为 ),依次表示数列的第项。
接下来行,每行包含两个整数,表示查询的区间为
输出格式
输出包含行,每行一个整数,依次表示每一次询问的结果。
输入样例
8 8
9 3 1 7 5 6 0 8
1 6
1 5
2 7
2 6
1 8
4 8
3 7
1 8
输出样例
9
9
7
7
9
8
7
9
思路
如果用传统的思路去写这道题,那么无非就是每次都对区间进行一次遍历,那么复杂度将高达,那一定会T掉,所以我们期望是先进行一遍预处理,然后每次通过或者是的复杂度进行查询,从而挤进去。
倍增
这边,则需要引入一个倍增的思想,利用倍增进行预处理,则可大大节省查询所需的时间,也就是查询可以达到的复杂度。
为了达到的查询时间复杂度,那么我们就需要预处理一个表,蕴含所有区间的结果。
而倍增的独特之处在于,他在的时候时间复杂度仅仅只需要,而查询也只需要。
定义
为了预处理出来一个表,那么我们先定义一下这个表的含义。
我们定义一个二维数组:是从开始,一直到这个区间的最大(小)值,也就是
那么这个有什么用呢,先不着急,我们先想办法预处理出来这个表。
预处理
设我们的原数组是a[]
,那么首先很显然的是
之后我们在进行递推,这边有一个公式
乍一看可能不是那么好理解,其实根据定义再结合一下几个直观的数字便可很容易明白他在做什么。
我们首先需要明白的是,一个区间内,不管是最小值还是最大值,该区间的最大(小)值都等于其所有子区间的最大(小)值的最大(小)值,即满足
那么上述公式也很好理解了,根据定义st[i][j]
是从开始到这个区间内的最大(小)值,所以他可以裂开成两个子区间,为了能够递推,所以我们裂开平分出两个长度为的子区间,很显然,这两个区间分别为和,也即,因此这两个区间恰好把原区间给分开了。
剩下的便是在代码上的处理了,代码不难理解,直接上吧。
#define rep(i,a,b) for (int i = a;i <= b;i ++)
const int MAXN = 100010;
int st[MAXN][20];
int a[MAXN];
void init() {
// 定义 st[i][j] 是从i开始,到i + 2^j这一段,即[i,i + 2^j]这一段中的最大/小值
rep(i,1,n) st[i][0] = a[i];
for (int j = 1;(1 << j) <= n;j ++) { // 遍历所有的j,j是一个很小的数字,最大值=log2(n)
rep(i,1,n - (1 << j) + 1) { // 在[1,n]区间范围内,确定j的情况下,把所有的i都遍历求值一遍
st[i][j] = max(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]); // 套公式
}
}
}
查询
查询这边也相应着有一个公式,若需要查询的区间左端下标是,右端是,则有
解释
我们想查询的区间是,也就是,st表的定义是
那么上述公式中
若,那么上述公式描述的均是。
而实际上,而,
所以答案只需要求和这两段的区间最大值即可。
查询代码
int query(int l, int r)
{
int x = log2(r - l + 1);
return max(st[l][x],st[r - (1 << x) + 1][x]);
}
st模版
#include
#include
#include
#include
#include
#include
#include
#include // pair
#include