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 预处理:建立"超级索引"
预处理就像为图书馆的每本书建立详细索引,步骤如下:
-
初始化基础区间(k=0,长度1):
dp[s][0] = 数组第s个元素(每个元素本身就是长度1的区间最值) -
填充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]的"倍增区间":
- 计算区间长度:
len = R - L + 1 - 找到最大的k:
k = log2(len)(满足2ᵏ ≤ len) - 取两个区间的最值:
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]中最高牛与最矮牛的身高差。
解题思路:
- 用两个ST表分别预处理区间最大值和最小值
- 每次查询计算 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…
题目大意:模拟隧道破坏与修复过程,每次查询包含某点的最长连续完好隧道长度。
解题思路:
- 用ST表维护区间最大连续前缀、后缀和整体长度
- 结合二分查找优化查询
提示:本题需要对ST算法进行扩展,存储更多区间信息,适合进阶练习。
八、例题提交注意事项
- 数据范围:注意数组大小是否需要开全局变量(如POJ需要避免栈溢出)
- 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; } } - 多组测试数据:部分题目需要处理多组输入,注意初始化DP表
通过以上例题练习,你可以逐步掌握ST算法的核心思想和应用技巧。建议先完成模板题,再挑战综合应用题,加深对算法的理解。