倍增法和 ST 表
RMQ 问题
来看看这样一个问题:
给定一个序列
每次询问的内容是求 {} 的 最大值。
这类问题我们将其称为 Range Minimum/Maximum Query,简称 RMQ。
我们知道连续子段和是可以通过前缀和优化去降低时间复杂度的,但是最值这个概念很明显是不能的,所以我们需要想想其他办法。
倍增思想
请注意,这一章里面的所有问题,都是 静态 的。动态的相关问题自然也是可做的,但是会使用到一些复杂的数据结构,比如说线段树,树状数组,甚至是动态树等等,这里就不再多说了。
我们需要找一个连续序列元素的最值,那么我们分别先求出左半序列的最值和右半序列的最值,然后在取出的两个最值中再取一次最值就行了。
根据这样一个思想,接下来我们来看看 ST 算法。
ST 算法预处理
在使用动态规划求区间最值时,有状态转移方程
相当于每一次转移增加 1 的长度。
在 ST 算法中,可以使用倍增的思想——每次转移长度翻倍。
用 表示以 i 为起点,长度为的区间的最值,即区间 。
在求解 ,可以把这个区间等分成两个区间,
即 和
以求最大值为例,状态转移方程为
当 j=0 时, 即 的最大值,即 。
在根据状态转移方程递推时,先求所有区间长度为 1 的区间最值,之后再求区间长度为 2 的区间最值、区间长度为 4 的区间最值 ⋯ 在求解区间长度为 2logn 的区间最值后,算法结束。
ST 算法询问
在预处理时,每一个状态对应的区间长度都为 。但给出的待查询区间长度不一定恰好为 2 的幂,因此需要对待查询的区间进行一些处理。
把待查询的区间分成两个小区间,这两个小区间满足两个条件:
- 这两个小区间的并集为待查询的区间
- 为了利用预处理的结果,要求小区间长度相等且都为 2 的幂。
注意两个小区间可能重叠。因为是求最值,所以重复计算并不影响结果。
我们用蓝色表示待查询区间 。假设 ,那么两个小区间(橘色)的长度就应该是 。而为了保证两个小区间的并集正好是蓝色区间,它们应该满足:
- 其中一个区间的左端是 l。
- 另一个区间的右端是 r。
所以这两个小区间分别是 和 。而我们 f 数组里面只存了区间的起点,所以最后用来求最值的量应该是和 。
比如待查询的区间为 ,区间长度为 15−3+1=13,那么取 ,两个小区间为 和 ,分别对应 和 ,也就是说答案是
参考代码
#include <cmath>
#include <iostream>
using namespace std;
const int maxn = 1000005;
int n, m, a[maxn], f[maxn][25], l, r;
int Log2[maxn];
void prepare() {
cin >> n >> m;
for (int i = 2; i <= n; i++) {
Log2[i] = Log2[i / 2] + 1;
}
for (int i = 1; i <= n; i++) {
cin >> a[i];
f[i][0] = a[i];
}
for (int j = 1; (1 << j) <= n; j++) {
for (int i = 1; i + (1 << j) - 1 <= n; i++) {
f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
}
}
}
int query(int l, int r) {
int i = Log2[r - l + 1];
return max(f[l][i], f[r - (1 << i) + 1][i]);
}
int main() {
prepare();
for (int i = 1; i <= m; i++) {
cin >> l >> r;
cout << query(l, r) << endl;
}
return 0;
}
ST 表
这道题我们需要去解决子段最大值的问题。
#include <cmath>
#include <iostream>
using namespace std;
const int maxn = 1000005;
int n, m, a[maxn], f[maxn][25], l, r;
int Log2[maxn];
void prepare() {
cin >> n >> m;
for (int i = 2; i <= n; i++) {
Log2[i] = Log2[i / 2] + 1;
}
for(int i = 1; i <= n; i++){
cin >> a[i];
f[i][0] = a[i];
}
for(int j = 1; (1 << j) <= n; j++){
for(int i = 1; i + (1 << j) - 1 <= n; i++){
f[i][j] = max(f[i][j - 1],f[i+(1 << (j - 1))][j-1]);
}
}
}
int query(int l, int r) {
int i = Log2[r - l + 1];
return max(f[l][i],f[r - (1 << i) + 1][i]);
}
int main() {
prepare();
for (int i = 1; i <= m; i++) {
cin >> l >> r;
cout << query(l, r) << endl;
}
return 0;
}