倍增法和 ST 表

163 阅读3分钟

倍增法和 ST 表

RMQ 问题

来看看这样一个问题:

给定一个序列a=a0,a1,...,an1a = {a_0,a_1,...,a_{n-1}}

每次询问的内容是求 {al,...ara_l,...a_r} 的 最大值。

这类问题我们将其称为 Range Minimum/Maximum Query,简称 RMQ。

我们知道连续子段和是可以通过前缀和优化去降低时间复杂度的,但是最值这个概念很明显是不能的,所以我们需要想想其他办法。

倍增思想

请注意,这一章里面的所有问题,都是 静态 的。动态的相关问题自然也是可做的,但是会使用到一些复杂的数据结构,比如说线段树,树状数组,甚至是动态树等等,这里就不再多说了。

我们需要找一个连续序列元素的最值,那么我们分别先求出左半序列的最值和右半序列的最值,然后在取出的两个最值中再取一次最值就行了。

根据这样一个思想,接下来我们来看看 ST 算法。

ST 算法预处理

在使用动态规划求区间最值时,有状态转移方程

f[i][j]=max(f[i][j1],aj)f[i][j] = max(f[i][j-1],a_j)

相当于每一次转移增加 1 的长度。

在 ST 算法中,可以使用倍增的思想——每次转移长度翻倍。

用 f[i][j]f[i][j]表示以 i 为起点,长度为2j2^j的区间的最值,即区间 [i,i+2j1][i,i+2^j-1]

image.png

在求解 f[i][j]f[i][j],可以把这个区间等分成两个区间,

[i,i+2j11][i,i+2^{j-1}-1]  和[i+2j1,i+2j1][i + 2^{j -1},i+2^j-1] 

以求最大值为例,状态转移方程为

f[i][j]=max(f[i][j1],f[i+2j1][j1])f[i][j] = max(f[i][j-1],f[i+2^{j-1}][j-1])

当 j=0 时, f[i][j]f[i][j] 即 [i,i][i,i] 的最大值,即 aia_i 。

在根据状态转移方程递推时,先求所有区间长度为 1 的区间最值,之后再求区间长度为 2 的区间最值、区间长度为 4 的区间最值 ⋯ 在求解区间长度为 2logn 的区间最值后,算法结束。

ST 算法询问

在预处理时,每一个状态对应的区间长度都为2j 2^j  。但给出的待查询区间长度不一定恰好为 2 的幂,因此需要对待查询的区间进行一些处理。

把待查询的区间分成两个小区间,这两个小区间满足两个条件:

  1. 这两个小区间的并集为待查询的区间
  2. 为了利用预处理的结果,要求小区间长度相等且都为 2 的幂。

注意两个小区间可能重叠。因为是求最值,所以重复计算并不影响结果。

image.png

我们用蓝色表示待查询区间 [l,r][l,r]。假设 2krl+1<2k+12^k≤r−l+1<2^k+1,那么两个小区间(橘色)的长度就应该是 2k2^k。而为了保证两个小区间的并集正好是蓝色区间,它们应该满足:

  1. 其中一个区间的左端是 l。
  2. 另一个区间的右端是 r。

所以这两个小区间分别是 [l,l+2k1][l,l+2^{k}-1]和 [r2k+1,r][r−2^k + 1,r]。而我们 f 数组里面只存了区间的起点,所以最后用来求最值的量应该是fl,kf_{l,k}fr2k+1,kf_{r-2^k+1,k} 。

比如待查询的区间为 [3, 15][3, 15],区间长度为 15−3+1=13,那么取 i=3 i=3 ,两个小区间为 [3,10][3,10] 和[8, 15][8, 15] ,分别对应 f3,3f_{3,3}f8,3f_{8,3} ,也就是说答案是  max(f3,3,f8,3)max(f_{3,3},f_{8,3})

参考代码

#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;
}

exm 题单