NeFut Logo NeFut
EN 管理员登录

高效静态区间最值查询:动态规划与倍增算法的深入解析

发布于:2026-05-27 09:17 最后更新:2026-06-26 02:00
#Dynamic Programming #Sparse Table #RMQ

核心逻辑与数学原理

1. 问题背景

给定一个长度为 $N$ 的固定序列,进行 $M$ 次区间最值查询(RMQ):

2. 幂等性与可重复贡献

ST 表(Sparse Table)的本质是基于动态规划思想的倍增算法,用于解决静态区间最值查询问题。

其数学基石在于覆盖可重复贡献性质(Idempotent Operations),即对于任意满足 $x * x = x$ 的二元幂等运算符 $*$(如 $\max, \min, \gcd$ 以及位运算中的 $\&, |$),多个重叠子区间的运算结果等于其并集的运算结果。

概念拆解:“幂”指乘方,“等”指相等。幂等的字面意思就是“高次幂的结果与一次幂相同”。其核心思想是:操作一次和操作无数次,最终结果完全一样。

3. 常数级空间覆盖

对于任意查询区间 $[L, R]$,其长度为 $len = R - L + 1$。令 $k = \lfloor \log_2(len) \rfloor$。

由于满足 $2^k \le len < 2^{k+1}$,区间 $[L, R]$ 必然能被两个长度为 $2^k$ 且分别以 $L$ 为起点、 $R$ 为终点的子区间完全覆盖(中间允许重叠)。其集合关系表示为:

$$[L, R] = [L, L + 2^k - 1] \bigcup [R - 2^k + 1, R]$$

基于幂等性,重叠部分不会影响最终的最值结果。由此,整个 RMQ 查询简化为常数级的代数映射,直接达成 $O(1)$ 查询:

$$\text{RMQ}(L, R) = \max(f[L][k], f[R - 2^k + 1][k])$$


状态设计与算法推导

1. 状态定义与倍增递推

在状态定义中:

设 $f[i][j]$ 表示区间 $[i, i + 2^j - 1]$ 长度为 $2^j$ 的子区间最值。利用倍增思想,当前状态(长度为 $2^j$ 的大区间)由上一层 $j-1$ 阶段的两段长度为 $2^{j-1}$ 的子区间递推合并(翻倍)而来:

状态转移方程为:

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

2. 拓扑序与循环顺序

在编写预处理代码时,外层循环必须枚举幂次 $j$,内层循环枚举下标 $i$。

计算当前层 $j$ 的大区间状态,必须依赖于上一层 $j-1$ 已经计算完毕的小区间状态。

3. $\log$ 数组常数级优化

为了在查询阶段真正达成 $O(1)$ 的极致速度,必须消除循环迭代,并避免直接调用高代价且存在精度风险的数学库 std::log2() 函数。

推荐的做法是在预处理阶段,通过线性时间 $O(N)$ 递推计算出 $1 \sim N$ 所有可能区间的 $\log_2$ 下取整值。查询时直接查表即可:

// 预处理 log 数组
lg[1] = 0;
for (int i = 2; i <= n; i++) {
    lg[i] = lg[i >> 1] + 1;
}

通过上述优化,在查询时仅需执行 int k = lg[len];,即可完美保障常数级响应。


const int MAXN = 100005; // 根据题目数据范围调整
const int MAXJ = 21;     // log2(MAXN) + 2

int n;
int a[MAXN];
int lg[MAXN];
int st[MAXN][MAXJ];

// ST 表预处理:O(N log N)
void init() {
    // 1. 线性递推预处理 log 数组
    lg[1] = 0;
    for (int i = 2; i <= n; i++) {
        lg[i] = lg[i >> 1] + 1;
    }

    // 2. 注入 DP 初始状态(1-indexed)
    for (int i = 1; i <= n; i++) {
        st[i][0] = a[i];
    }

    // 3. 核心倍增 DP:外层枚举幂次,内层枚举下标
    for (int j = 1; j <= lg[n]; j++) {
        for (int i = 1; i + (1 << j) - 1 <= n; i++) {
            st[i][j] = max(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]);
        }
    }
}

// O(1) 区间最值查询
inline int query(int l, int r) {
    int len = r - l + 1;
    int k = lg[len];
    return max(st[l][k], st[r - (1 << k) + 1][k]);
}

NOIP 实战避坑指南

  1. 循环顺序颠倒与边界溢出
  1. 位运算优先级与常数灾难

经典 NOIP/洛谷 真题

洛谷 P3865 【模板】ST 表

核心算法代码
// P3865 核心代码:单指标最大值 ST 表
int lg[MAXN], st[MAXN][MAXJ];

void init(int n, int a[]) {
    lg[1] = 0;
    for (int i = 2; i <= n; i++) lg[i] = lg[i >> 1] + 1;
    for (int i = 1; i <= n; i++) st[i][0] = a[i];

    for (int j = 1; j <= lg[n]; j++) {
        for (int i = 1; i + (1 << j) - 1 <= n; i++) {
            st[i][j] = max(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]);
        }
    }
}

inline int query(int l, int r) {
    int k = lg[r - l + 1];
    return max(st[l][k], st[r - (1 << k) + 1][k]);
}

洛谷 P2880 [USACO07JAN] Balanced Lineup G

核心算法代码
// P2880 核心代码:双指标最大/最小值 ST 表
int lg[MAXN];
int st_max[MAXN][MAXJ], st_min[MAXN][MAXJ];

void init(int n, int a[]) {
    lg[1] = 0;
    for (int i = 2; i <= n; i++) lg[i] = lg[i >> 1] + 1;
    for (int i = 1; i <= n; i++) st_max[i][0] = st_min[i][0] = a[i];

    for (int j = 1; j <= lg[n]; j++) {
        for (int i = 1; i + (1 << j) - 1 <= n; i++) {
            st_max[i][j] = max(st_max[i][j - 1], st_max[i + (1 << (j - 1))][j - 1]);
            st_min[i][j] = min(st_min[i][j - 1], st_min[i + (1 << (j - 1))][j - 1]);
        }
    }
}

inline int query_diff(int l, int r) {
    int k = lg[r - l + 1];
    int mx = max(st_max[l][k], st_max[r - (1 << k) + 1][k]);
    int mn = min(st_min[l][k], st_min[r - (1 << k) + 1][k]);
    return mx - mn;
}

[h] 返回首页