RMQ问题之ST表

本文介绍了如何从暴力求解区间最大值问题,通过打表优化到使用稀疏表(ST表)进行预处理,实现从O(mn)到O(nlogn)的时间复杂度提升。重点讲解了ST表的概念、构建过程以及如何用它解决区间最值查询问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

说明:优快云和公众号文章同步发布,需要第一时间收到最新内容,请关注公众号【比特正传】。

数据结构和算法是计算机的灵魂,编写数据结构的代码,对学生代码能力的提升是巨大的,看到好几位NOI金牌得主的分享,很多选手都是在编写数据结构相关代码(线段树、平衡树等等,后期都会分享)中代码能力得到了巨大提升,因此数据结构尤为重要。之前分享过一篇数据结构的文章Trie树,欢迎大家阅读。

数据结构:Trie树(字典树)就是这么简单

0、概念梳理

RMQ(Range Minimum/Maximum Query):区间最值查询。

ST(Sparse Table)表:稀疏表,不支持动态修改数组,适用于查找静态不变数组的最值问题。

位运算:我们熟知的幂运算,比如2^n,在c++中,我们可以通过位运算完成,即2^n 等价于 1<<n。

1、问题描述

2、方法一:暴力做法

暴力做法的思路和代码都很简单,题目询问哪个区间,就去遍历该区间,找最大值,时间复杂度为O(mn),代码如下:

#include<bits/stdc++.h>
// 暴力方法:每次询问区间[l, r],遍历该区间找最大值 
using namespace std;
const int N = 1e5+7;
int n, m, l, r, a[N];
int query() {
    int maxn = a[l];
    // 万能找最大值/最小值模板,先令第一个元素最大/最小,然后遍历其他元素
    // 如果有比当前最大值/最小值更大的/更小的,那么更新当前最大值/最小值 
    for(int i=l+1; i<=r; i++) maxn = max(maxn, a[i]);
    return maxn;
}
?
int main() {
    scanf("%d%d", &n, &m); 
    for(int i=1; i<=n; i++) scanf("%d", &a[i]); 
    while(m--) {
        scanf("%d%d", &l, &r); 
        printf("%d\n", query());
    }
    return 0;
}

很明显,只能拿部分分数,8个测试点超时了。

3、方法二:打表

f[i][j]表示区间[i,j]的最大值,可以通过已有的小区间 f值 填充更大的区间的 f值,从而完成对f数组的预处理,在m次询问环节,可以直接返回,时间复杂度为O(n^2)。


#include<bits/stdc++.h>
// 暴力方法:每次询问区间[l, r],遍历该区间找最大值 
using namespace std;
const int N = 1e5+7;
int n, m, l, r, a[N];
int main() {
    scanf("%d%d", &n, &m); 
    int f[n+5][n+5]; // f[i][j]表示区间[i, j]的最大值 
    for(int i=1; i<=n; i++) {
        scanf("%d", &a[i]); 
        f[i][i] = a[i];  // 初始化 
    }
    for(int i=1; i<n; i++) {
        for(int j=i+1; j<=n; j++) {
            f[i][j] = max(f[i][j-1], a[j]);  // 用子区间求当前区间的最大值 
        } 
    } 
    while(m--) {
        scanf("%d%d", &l, &r); 
        printf("%d\n", f[l][r]);
    }
    return 0;
}

4、方法三:ST 表

思考一下方法二,虽然用了f数组,但是递推式

f[i][j] = max(f[i][j-1], a[j])

相当于是一步一步推导出来的,依然是线性,那如果把区间一分为二,即可变成对数log级别,大大提高效率。

上面方法2中,f[i][j]表示区间[i,j]的最大值,现在重新定义f[i][j]:以i为起点,长度为2^j的区间,如此一来,f数组的第二维就很小了,比如方法二中的f数组第二维应该是n(n<=10^5),由于是二维数组,所以整个数组的大小为10^10,这个数组占多少空间呢?(10^10 * 4) / (2^30)约等于37.3GB,但此时第二维的大小就是log2(n),n取最大值10^5时,log2(n)也不会超过17,所占内存为:10^5 * 17 * 4 / 2^20 约等于6.5MB,省去很大空间。

那如何更新f数组呢?

最外层循环是长度,里层循环是起点,先更新所有长度为1的区间f[i][0],

再更新所有长度为2的区间f[i][1],然后是长度为4的区间f[i][2], 长度为8的区间f[i][3]......

那么f[i][j]表示起点为i,长度为2^j的区间,可以把这个区间一分为2,两个子区间的长度均为2^(j-1),此时左区间的端点不变,依然是i,那么可以通过左区间的左端点和左区间的长度,计算出右区间的左端点,右区间的左端点等于 左区间的左端点+左区间的长度,即等于 i + 2^(j-1),用c++的位运算可以写成 i+(1<<j-1),此时左区间的最大值为f[i][j-1], 右区间的最大值为f[i+(1<<j-1)][j-1],因此:

f[i][j] = max(f[i][j-1], f[i+(1<<j-1)][j-1]);

比如任意一个区间,[3,13],要计算这个区间的最大值,如何用上面的f数组表示呢?

f[3][3]表示的是以3为起点,长度为8(2^3)的区间(即区间[3,10])中的最大值。

f[6][3]表示的是以6为起点,长度为8(2^3)的区间(即区间[6,13])中的最大值。

那么区间[3,13]中的最大值等于max(f[3][3], f[6][3]),所以可以通过O(1)的复杂度计算出区间的最大值。

因此对于任意一个区间,我都可以找到两个可能会重合的子区间来表示。那么到底是怎么找的呢?

比如对于一个区间[x, y],如何用刚才那种方式找到两个子区间呢?

步骤如下:

1、首先计算区间长度len = y - x + 1;

2、计算对数 j = (int)log2(len); 并向下取整转为int;

3、第一个区间为:[x, x+2^j],这个区间的最大值就是f[x][j];

4、第二个区间为:[y - 2^j + 1, y],这个区间的最大值就是f[y - 2^j + 1][j],所以区间[x, y]的最大值为:max(f[x][j], f[y - 2^j + 1][j])。


#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+7, T = 18;
int n, m, l, r, a[N], f[N][T];

int query() {
    int len = r - l + 1;
    int j =(int)log2(len);  // log2 函数在cmath库中
    // 两个区间可以重叠,不影响max 和 min 
    return max(f[l][j], f[r-(1<<j)+1][j]); 
}

int main() {
    scanf("%d%d", &n, &m); 
    for(int i=1; i<=n; i++) {
        scanf("%d", &a[i]); 
        f[i][0] = a[i];  // 初始化以i为起点,长度为1的区间的最大值,即为a[i]本身 
    }
    for(int j = 1; j <= (int)log2(n); j++) {
        int tmp = (1<<j)-1;
        for(int i = 1; i + tmp <= n; i++) {
            // 计算以i为起点,长度为2^j的区间的最大值
            f[i][j] = max(f[i][j-1], f[i+(1<<j-1)][j-1]);
        }
    }
    while(m--) {
        scanf("%d%d", &l, &r); 
        printf("%d\n", query());
    }
    return 0;
}

图片

ST表的预处理时间复杂度为O(nlogn),查询时间复杂度为O(1)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值