//2024.4.9 初稿,后续会完善更新。
//2024.4.10 第一次修改
//2024.4.11 第二次修改
引入
如果给你一个数组,要你输出[l, r]区间中的最大值,你会怎么做?
我会线段树
抛开线段树不谈,按正常的操作是[l, r]上遍历一遍暴力取最大,但是这太不方便了,智慧的人类想到了能够O(1)查询的方法,但是这容忍了区间的重复----ST表。
什么时候会用到
ST表是解决 可重复贡献问题 的数据结构,
常见的有区间值、区间按位和、区间按位或、区间GCD等。
用在静态,区间,可重复问题上。
什么叫可重复贡献度? 比如要你求 10 个数的最大值,你完全可以先求前 6 个的最大值,然后再求后 7 个的最大值,然后两个取最大。在此过程中有些数被重复利用到了,这就是可重复贡献度问题。
构建
代表着从第 i 号点开始, 长度为 的区间的性质。
以按位与为例,
构建
inline void build() {
for (int i = 1; i <= n; i ++ ) st[i][0] = a[i];
for (int j = 1; (1 << j) <= n; j ++ ) {
for (int i = 1; i + (1 << j - 1) <= n; i ++ ) {
st[i][j] = st[i][j - 1] & st[i + (1 << j - 1)][j - 1];
}
}
}
首先,st[i][0]代表长度为 0, 那么就是单个数据的本身性质。
类似于线段树的pushup操作,j 是从 两个 j - 1上推出来的
这里的 j 指代的是区间的长度,
j 最大枚举到 lg2(n)即可,因为查询的时候,用两个lg2(n)的长度的区间就可以覆盖整个区间。
[l, r]
[---------------- ]
[ ------------------------]
而下面的 i + (1 << j - 1) 是要确保第二个点在区间内,因为我们不需要在区间外的点作为st[i]中的i
然后我们开始构建,有转移方程:
开的大小:第一维开区间长度,第二维开lg2(n)即可,这是因为我们用倍增进行了优化,如果按正常存储,我们需要开的空间,这样会爆的。
查询
对于每个询问, 我们将其分成和两部分进行查询。
(其中)
这样的s会有重叠,但是重叠对答案并无影响。
下面的式子取出了[l, r]上的按位与。
inline void query(int l, int r) {
int s = lg2[r - l + 1];
return st[l][s] & st[r - (1 << s) + 1][s];
}
lg2预处理
我们会需要用到lg2,直接预处理可以节省时间。
inline void pre_work() {
for (int i = 2; i <= n; i ++ ) lg2[i] = lg2[i >> 1] + 1;
}
相关题目
模板题+代码:
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
using ll = long long;
const int N = 1e5 + 9;
int n, m, a[N], lg2[N], st[N][20];
inline void build() {
for (int i = 1; i <= n; i ++ ) st[i][0] = a[i];
for (int j = 1; (1 << j) <= n; j ++ ) {
for (int i = 1; i + (1 << j - 1) <= n; i ++ ) {
st[i][j] = max(st[i][j - 1], st[i + (1 << j - 1)][j - 1]);
}
}
}
inline void pre_work() {
for (int i = 2; i <= 1e5; i ++ ) lg2[i] = lg2[i >> 1] + 1;
}
inline int query(int l, int r) {
int s = lg2[r - l + 1];
return max(st[l][s], st[r - (1 << s) + 1][s]);
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> a[i];
pre_work(), build();
while (m -- ) {
int l, r; cin >> l >> r;
cout << query(l, r) << endl;
}
return 0;
}