ST算法:区间最值查询的"闪电侠"

109 阅读7分钟

ST算法:区间最值查询的"闪电侠"

一、什么是ST算法?

想象你是一位图书馆管理员,每天需要回答读者"某类书籍中哪本最厚"的问题。如果每次都要逐本测量,效率会非常低下。ST算法就像为图书馆建立了一套智能索引系统,让你能在1秒内找到任意区间的"最厚书籍"——这就是它在区间最值查询(RMQ)问题中的强大之处。

ST(Sparse Table)算法是一种基于倍增思想动态规划的高效查询算法,特别适合解决静态数组的区间最值查询问题。它的预处理时间复杂度为O(N log N),而每次查询仅需O(1)时间,堪称区间查询领域的"闪电侠"。

二、核心思想:用"跳台阶"的智慧覆盖区间

2.1 倍增思想:一次跳得更远

ST算法最精妙的地方在于倍增划分。我们可以把它比作爬楼梯:如果每次只能跨1级台阶,爬到第16级需要16步;但如果每次能跨前一次两倍的距离(1→2→4→8→...),只需4步就能到达!

在ST算法中,我们将数组划分为长度为2⁰, 2¹, 2², ..., 2ᵏ的区间,就像给数组盖上不同尺寸的"印章":

  • 尺寸1(2⁰):[1], [2], [3], ... (单个元素)
  • 尺寸2(2¹):[1-2], [3-4], [5-6], ... (相邻2个元素)
  • 尺寸4(2²):[1-4], [5-8], [9-12], ... (相邻4个元素)
  • 以此类推,直到覆盖整个数组

2.2 动态规划:构建区间最值的"俄罗斯套娃"

ST算法用一个二维数组dp[s][k]存储区间最值,其中s是区间起点,k表示区间长度为2ᵏ。这个数组就像一套"俄罗斯套娃",大区间的最值由小区间的最值组合而成:

定义dp[s][k] = 从s开始,长度为2ᵏ的区间的最值
转移方程dp[s][k] = max(dp[s][k-1], dp[s + 2^(k-1)][k-1])

举个例子,dp[1][2](起点1,长度4的区间)可以由:

  • dp[1][1](起点1,长度2)和
  • dp[3][1](起点3,长度2)
    两个小区间的最值组合而成。就像两个小盒子拼在一起变成一个大盒子!

三、ST算法实战:两步打造查询神器

3.1 预处理:建立"超级索引"

预处理就像为图书馆的每本书建立详细索引,步骤如下:

  1. 初始化基础区间(k=0,长度1):
    dp[s][0] = 数组第s个元素(每个元素本身就是长度1的区间最值)

  2. 填充DP表(k从1到log2(N)):
    对于每个可能的区间长度2ᵏ,计算所有起点s的区间最值:

    for (int j = 1; j <= k_max; j++) {       // k_max = log2(n)
        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]);
        }
    }
    

3.2 查询:O(1)时间的"闪电搜索"

查询任意区间[L, R]时,我们只需找到两个能覆盖[L, R]的"倍增区间":

  1. 计算区间长度len = R - L + 1
  2. 找到最大的kk = log2(len)(满足2ᵏ ≤ len)
  3. 取两个区间的最值
    max(dp[L][k], dp[R - 2ᵏ + 1][k])

例如,查询[1,5]时:

  • len=5,k=2(2²=4 ≤5)
  • 第一个区间:[1,4](dp[1][2])
  • 第二个区间:[2,5](dp[2][2])
  • 两个区间的并集覆盖[1,5],取它们的最值即可!

四、C++完整实现:从代码到理解

下面是ST算法的完整C++代码,包含详细注释:

#include <iostream>
#include <vector>
#include <cmath>
using namespace std;

// 快速读入函数(处理大数据输入)
inline int read() {
    char c = getchar();
    int x = 0, f = 1;
    while (c < '0' || c > '9') {
        if (c == '-') f = -1;
        c = getchar();
    }
    while (c >= '0' && c <= '9') {
        x = x * 10 + c - '0';
        c = getchar();
    }
    return x * f;
}

int main() {
    int n = read(), m = read();  // n:数组长度, m:查询次数
    vector<int> num(n);
    for (int i = 0; i < n; i++) {
        num[i] = read();  // 读取数组元素
    }

    // 预处理log2值,避免重复计算
    int k_max = log2(n) + 1;
    vector<vector<int>> dp(n + 1, vector<int>(k_max + 1, 0));  // dp[起点][k]

    // 初始化:区间长度为1(2^0)的情况
    for (int i = 1; i <= n; i++) {
        dp[i][0] = num[i - 1];  // 注意数组下标从0开始
    }

    // 填充DP表:计算所有可能的区间长度
    for (int j = 1; j <= k_max; j++) {
        for (int i = 1; i + (1 << j) - 1 <= n; i++) {  // 确保区间不越界
            int half = 1 << (j - 1);  // 2^(j-1)
            dp[i][j] = max(dp[i][j-1], dp[i + half][j-1]);
        }
    }

    // 处理m次查询
    while (m--) {
        int L = read(), R = read();
        int len = R - L + 1;
        int k = log2(len);  // 最大的k,满足2^k <= len
        int max_val = max(dp[L][k], dp[R - (1 << k) + 1][k]);
        printf("%d\n", max_val);
    }

    return 0;
}

代码关键细节:

  • 位运算加速1 << j 等价于2ʲ,比pow(2,j)更快
  • log2预处理:提前计算最大k值,避免重复调用log2()
  • 边界控制i + (1 << j) - 1 <= n 确保区间不越界

五、ST算法的应用与局限

适用场景:

  • 静态数组:数组元素不修改(如果需要修改,考虑线段树)
  • 多次查询:预处理一次,支持O(1)快速查询
  • 最值问题:最大值/最小值查询(不支持求和、乘积等)

典型应用:

  • 区间最大/最小值查询(RMQ问题)
  • LCA(最近公共祖先)问题的预处理
  • 竞赛中的快速查询优化

六、总结:ST算法的"武功秘籍"

ST算法之所以高效,是因为它将倍增思想(快速覆盖)和动态规划(复用子问题结果)完美结合:

  • 预处理:O(N log N)时间,构建"超级索引"
  • 查询:O(1)时间,闪电般获取结果

记住这个核心公式,你就掌握了ST算法的精髓:
dp[s][k] = max(dp[s][k-1], dp[s + 2^(k-1)][k-1])

下次遇到区间最值查询问题,不妨试试ST算法这个"闪电侠",让你的代码效率飙升!## 七、例题实战:从理论到实践

例题1:洛谷P3865 【模板】ST表

题目链接www.luogu.com.cn/problem/P38…
题目大意:给定一个长度为N的数组,进行M次查询,每次查询区间[L, R]的最大值。
解题思路:直接套用ST算法模板,预处理后O(1)查询。
关键代码片段

// 查询区间[L, R]的最大值
int query_max(int L, int R) {
    int len = R - L + 1;
    int k = log2(len);
    return max(dp[L][k], dp[R - (1 << k) + 1][k]);
}

难度:入门级(模板题)

例题2:POJ 3264 Balanced Lineup

题目链接poj.org/problem?id=…
题目大意:给定N头牛的身高,Q次查询区间[L, R]中最高牛与最矮牛的身高差。
解题思路

  1. 用两个ST表分别预处理区间最大值和最小值
  2. 每次查询计算 max_val - min_val
    核心代码
// 预处理最大值表和最小值表
for (int j = 1; j <= k_max; j++) {
    for (int i = 1; i + (1 << j) - 1 <= n; i++) {
        max_dp[i][j] = max(max_dp[i][j-1], max_dp[i + (1 << (j-1))][j-1]);
        min_dp[i][j] = min(min_dp[i][j-1], min_dp[i + (1 << (j-1))][j-1]);
    }
}

难度:基础级(ST算法的直接应用)

例题3:HDU 1540 Tunnel Warfare

题目链接acm.hdu.edu.cn/showproblem…
题目大意:模拟隧道破坏与修复过程,每次查询包含某点的最长连续完好隧道长度。
解题思路

  1. 用ST表维护区间最大连续前缀、后缀和整体长度
  2. 结合二分查找优化查询
    提示:本题需要对ST算法进行扩展,存储更多区间信息,适合进阶练习。

八、例题提交注意事项

  1. 数据范围:注意数组大小是否需要开全局变量(如POJ需要避免栈溢出)
  2. log2计算:建议预处理log数组避免精度问题:
    int log_table[100005];
    void pre_log() {
        log_table[1] = 0;
        for (int i = 2; i <= 100000; i++) {
            log_table[i] = log_table[i/2] + 1;
        }
    }
    
  3. 多组测试数据:部分题目需要处理多组输入,注意初始化DP表

通过以上例题练习,你可以逐步掌握ST算法的核心思想和应用技巧。建议先完成模板题,再挑战综合应用题,加深对算法的理解。