【ACM之路】9.RMQ问题 (ST表)

RMQ(Range Minimum / Maximum Query)指的是“区间最值问题”

即求多次查询区间最大值和最小的问题。

一般常用的解决方法有:

1.暴力:O(n^2)

2. ST表:预处理O(nlogn), 单次查询O(1),空间O(nlogn),只能静态查询,局限性大。

3. 线段树:预处理O(nlogn), 单词查询O(logn),空间O(n),可以静态和动态查询。

4. LCA和RMQ:笛卡尔数,LCA(least common ancestor)是最近公共祖先


Note:读到(1 << j)的时候就把他当成2^j次会更容易理解题意。

 

ST表

基于dp和倍增的思想。我们用二维数组dp[i][j]表示从第i位开始,往后连续2^j的区间的最小值。

初始条件:dp[i][0] = a[i].表示从第i位开始,共1位的最小值。也即a[i]本身了。

如何写状态转移方程呢?我们可以将2^j的区间分成两部分,每一部分都是2^(j - 1),即将[i, i + 2^j - 1] 分成[i, i + 2^(j - 1) - 1] 和 [i + 2^(j - 1), i + 2^j - 1]两部分。

状态转移方程可以写成dp[i][j] = min(dp[i][j - 1], dp[i + (1 << j - 1)][j - 1];

(把1<< j - 1换成2^(j - 1)次更直观理解。因为是分成两半了嘛,所以dp第二项都是j - 1次。第一个数从i开始,第二个数从i + 2^(j - 1)开始),如图所示。

总结:

初始条件:dp[i][0] = a[i]

状态转移方程:dp[i][j] = min(dp[i][j - 1], dp[i + (1 << j - 1)][j - 1].

预处理代码如下:

for (int j = 1; (1 << j) <= n; j++) {
    for (int i = 1; i + (1 << j) - 1 <= n; i++) {
        dp[i][j] = min(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);
    }
}

但是ST表如何查询某个区间的最值呢?

我们还是可以将区间分为两部分,设k = (int)log2(j - i + 1)。则每个分区间的长度为2^k。

ans = min(dp[i][k], dp[j - (1 << k) + 1][k])

查询过程如下。(C语言没有专门的求log2的函数,有log(x)代表以自然常数e为底求logx,需要用换底公式,设log(a,b)表示以a为底log(b),则log(a, b) = log(c, b) / log(c, a)。我们用c语言的log函数,以自然常数为底,则公式如下)

int query(int l, int r) {
    int k = log(r - l + 1.0) / log(2.0);
    return max(dp[l][k], dp[r - (1 << k) + 1][k]);
}

加入(l, r)区间的大小刚好是2的指数,则平分的子区间刚好是一半,也即2^(k-1).如果不是2的倍数,则区间会比2^(k-1)大,这会表示他们之间有重叠,但并不影响求最值。看图:

模板题:

洛谷P3865 : https://www.luogu.com.cn/problem/P3865

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
using namespace std;
int a[100005];
int dp[100005][30];
void init_ST(int n) {
    memset(dp, 0, sizeof(dp));
    for (int i = 1; i <= n; i++) {
        dp[i][0] = a[i];
    }
    for (int j = 1; (1 << j) <= n; j++) {
        for (int i = 1; i + (1 << j) - 1 <= n; i++) {
            dp[i][j] = max(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);
        }
    }
}
int query(int l, int r) {
    int k = log(r - l + 0.1) / log(2.0);
    return max(dp[l][k], dp[r - (1 << k) + 1][k]);
}
int main() {
    int n, m;
    while(~scanf("%d%d", &n, &m)) {
        for (int i = 1; i <= n; i++) {
            scanf("%d", &a[i]);
        }
        init_ST(n);
        while (m--) {
            int l, r;
            scanf("%d%d", &l, &r);
            printf("%d\n", query(l, r));
        }
    }
    return 0;
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值