← 目录 第 53 / 60 节 全部 60 节

53 · 倍增与 ST 表

把“走 2^k 步”预先存下来,任意步数就能拆成二进制去拼。本节用这一招做 O(1) 的静态区间最值(ST 表),以及 O(log n) 的树上 LCA,并给出 lg 数组、st 递推、up 跳表三张必须背下来的表。
提高预计 65 分钟ST 表倍增LCA
本节出现的新函数 / 新语法(先看懂再用)

倍增只有一个动作:先把“走 2k 步之后到哪”全部算出来,再把任意步数拆成二进制去拼。正整数的二进制分解唯一,所以 105 步最多拆成 17 段,每段长度都是 2 的幂。这一节用它做两件事:O(1) 的静态区间最值(ST 表)、O(log n) 的树上 LCA。

ST 表 = 预处理所有长度是 2 的幂的区间 + 每次查询用两段这样的区间拼出目标区间。建表 O(n log n),查询 O(1),代价是只能用于 max、min、gcd 这类“重复算不影响结果”的运算。
要做的事数据规模做法复杂度
静态区间最值(ST 表)n ≤ 105,m ≤ 2×106倍增预处理 2 的幂长度区间建表 O(n log n),单次 O(1)
树上两点 LCA(倍增)n, m ≤ 5×105up[k][v] 跳 2k 级祖先预处理 O(n log n),单次 O(log n)
树上第 k 级祖先n, m ≤ 5×105把 k 按二进制拆开跳单次 O(log n)
快速幂的指数拆分(第 37 节)b ≤ 1018同一个思想,底数平方递推单次 O(log b)

先把两个工具拆开。第一个是 lg 数组——它只有四行代码,但决定了查询能不能真的 O(1);第二个是 ST 表本身的定义与递推。

新函数拆解 · lg 数组预计算(floor(log2) 查表)
lg[i] = lg[i >> 1] + 1; // 预处理,O(n) int k = lg[r - l + 1]; // 查询时 O(1) 取出
组成含义 / 格式要求
lg[i] 的含义最大的非负整数 k,使 2k ≤ i。也就是 ⌊log2 i⌋,等于 i 的二进制位数减 1。
初始化lg[1] = 0(20 = 1 ≤ 1,而 21 = 2 > 1);lg[0] 用不到,全局数组默认 0 正好无害。循环从 i = 2 开始,不要从 1 开始,否则会把 lg[1] 改写成 1。
递推式lg[i >> 1] + 1。因为 i 与 ⌊i/2⌋ 的二进制位数最多差 1,而 ⌊log2 i⌋ = ⌊log2⌊i/2⌋⌋ + 1 对 i ≥ 2 恒成立(乘 2 = 指数加 1,奇数末位被取整吃掉)。
数组大小必须开到 n + 1。开成 n 时 lg[n] 越界,症状是随机的 RE 或答案错,而且往往小样例看不出来。
为什么不用 log2log2 是浮点函数,一次调用几十纳秒,2×106 次询问就是上百毫秒;而且它不保证对 2 的幂返回恰好整数,转 int 后可能少 1,直接查错层。查表既快又准。
__lg 的替代GCC 内建 __lg(x) 直接返回 ⌊log2 x⌋,x ≥ 1。但它不是标准 C++(MSVC 没有),且实测在密集查询里略慢于查表,竞赛里一般还是预计算。
最小可运行示例
示例
#include <bits/stdc++.h>
using namespace std;

int lg2v[10];

int main() {
    lg2v[1] = 0;                                  // 2^0 = 1
    for (int i = 2; i <= 8; ++i) lg2v[i] = lg2v[i >> 1] + 1;

    for (int i = 1; i <= 8; ++i) cout << i << " -> " << lg2v[i] << "\n";
    // 1 -> 0    2 -> 1    3 -> 1    4 -> 2
    // 5 -> 2    6 -> 2    7 -> 2    8 -> 3
    return 0;
}
注意

本文把这张表记作 lg;示例与完整代码里的数组名是 lg2v,两者是同一个东西。

lg2v[i] = lg2v[i >> 1] + 1 依赖 lg2v[⌊i/2⌋] 已经算好。因为 ⌊i/2⌋ < i,从小到大循环天然满足。

查询时长度是 r - l + 1,不是 r - l。差一个 1 会让 k 取小一层,结果只覆盖半个区间(WA,答案偏小)。

新函数拆解 · ST 表:st[k][i] 的建表与查询
st[k][i] = 区间 [i, i + 2^k - 1] 的答案 st[0][i] = a[i]; st[k][i] = max(st[k-1][i], st[k-1][i + (1 << (k-1))]); ans = max(st[k][l], st[k][r - (1 << k) + 1]); // k = lg[r-l+1]
组成含义 / 格式要求
st[k][i] 的定义以 i 为左端点、长度恰好 2k 的区间的答案。k 叫“层”,i 叫“起点”,两者一起唯一确定一个区间。
第 0 层长度 20 = 1,就是原数组。建表时可以直接把读入写进 st[0][i],省掉一个 a[] 数组。
递推怎么来长度 2k 的区间从中间劈成两个长度 2k-1 的半段:[i, i+2k-1-1] 与 [i+2k-1, i+2k-1],正好是 st[k-1][i] 与 st[k-1][i + 2k-1]。所以一行 max 就能合成。
合并只要求结合律 + 幂等同一元素算两次不影响结果(max、min、gcd、按位或、按位与都满足)。求和、乘积不满足,套进来就错。
查询的两个起点k = ⌊log2 len⌋。左段从 l 起(覆盖前 2k 个),右段以 r 结尾(起点 r − 2k + 1)。两段都落在 [l, r] 内。
为什么两段一定盖满由 k 的定义 2k ≤ len < 2k+1。若两段之间有缝,则 len > 2k+1,矛盾。所以要么重叠、要么恰好相接。
空间与写法st[K][n+1] 个 int。第一维放层、第二维放起点:查询时同一个 k 连查两个起点,行内地址连续,缓存命中率更高。
最小可运行示例
示例
// n = 1e5,K = 17(2^16 = 65536 <= 1e5 < 2^17)
int st[17][100005], lg2v[100005];

lg2v[1] = 0;
for (int i = 2; i <= n; ++i) lg2v[i] = lg2v[i >> 1] + 1;

for (int k = 1; (1 << k) <= n; ++k)                       // 只要 2^k <= n 就有合法区间
    for (int i = 1; i + (1 << k) - 1 <= n; ++i)           // 右端点不能越过 n
        st[k][i] = max(st[k-1][i], st[k-1][i + (1 << (k-1))]);

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

内层循环的右端点判断 i + (1 << k) - 1 <= n 不能省。省掉就是越界写,会踩坏后面的数组,症状是 RE 或者只有大数据才错。

把 max 换成 + 立刻 WA:查询时两段重叠,重叠部分会被加两次。这就是“只支持幂等运算”的实际含义。

这两块拼起来就是 ST 表模板。下面这道题把 m 堆到 2×106,正是逼你把单次查询压到 O(1) 的经典设置——线段树 O(log n) 会多出一个 17 的常数,在 3.4×107 次操作上就是过与不过的差别。

例题 · 【模板】ST 表洛谷 P3865普及/提高-去洛谷做这题 ↗

给定 n 个整数 a1…an,和 m 次询问,每次询问区间 [l, r] 内的最大值。

输入:第一行两个整数 n、m(1 ≤ n ≤ 105,1 ≤ m ≤ 2×106)。第二行 n 个整数,|ai| ≤ 109。接下来 m 行,每行 l、r(1 ≤ l ≤ r ≤ n)。

输出:m 行,每行一个整数,表示对应询问的答案。

输入: 8 3 9 3 1 7 5 2 6 4 1 6 2 8 3 3 输出: 9 7 1
例题怎么想:把“所有区间”砍到只剩 2 的幂

先估暴力:每次询问扫一遍 [l, r],最坏 O(n) 一次,总 O(nm) = 105 × 2×106 = 2×1011 次比较,按每秒 109 次算也要 200 秒。瓶颈不是每一步慢,是步数太多。

要 O(1),答案就必须“提前算好、查询时只做常数次查表”。所以问题变成:预处理哪些区间,才既装得下又够用?

所有区间有 n(n+1)/2 个,105 时约 5×109 个,装不下。于是只留长度是 2 的幂的区间:每一层 n 个,一共 17 层,1.7×106 个数,只占 6.8 MB。少算的代价由“两段拼接”补回来。

拼接的正确性:设 len = r − l + 1,k = ⌊log2 len⌋。长度 2k 的区间放得下,左起一段 [l, l+2k−1] 和右止一段 [r−2k+1, r] 都在 [l, r] 内;又因为 2k+1 > len,两段长度之和超过 len,中间不可能有缝。重叠部分被算了两次,但 max 对同一个数算两次没有影响。

层 k区间长度st[k][1..]说明
019 3 1 7 5 2 6 4原数组,一个数一段
129 3 7 7 5 6 6每两个取 max
249 7 7 7 6每四个取 max
389整个数组

走一遍询问 l = 2、r = 8:len = 7,k = ⌊log2 7⌋ = 2。左段 st[2][2] 覆盖 [2,5],值是 7;右段起点 8 − 4 + 1 = 5,st[2][5] 覆盖 [5,8],值是 6。答案 max(7, 6) = 7。区间 [5,5] 同时被两段覆盖(叠了一次),但 7 和 6 取 max 仍是 7,结果不受影响。

三步骨架
lg2v[1] = 0
for i = 2 .. n:  lg2v[i] = lg2v[i >> 1] + 1        // ① 长度 -> 层号

st[0][i] = a[i]                                     // ② 第 0 层就是原数组
for k = 1 .. while 2^k <= n:
    for i = 1 .. n - 2^k + 1:
        st[k][i] = max(st[k-1][i], st[k-1][i + 2^(k-1)])

query(l, r):
    k = lg2v[r - l + 1]                             // ③ 两段拼接
    return max(st[k][l], st[k][r - 2^k + 1])
完整代码
#include <bits/stdc++.h>
using namespace std;

const int N = 100005;
const int K = 17;                       // 2^16 = 65536 <= 1e5 < 2^17,层数最多到 16

int st[K][N];                           // st[k][i]:区间 [i, i + 2^k - 1] 的最大值
int lg2v[N];                            // lg2v[len] = floor(log2(len))

int main() {
    int n, m;
    scanf("%d %d", &n, &m);             // m 到 2e6,用 scanf/printf 比 cin/cout 稳

    for (int i = 1; i <= n; ++i) scanf("%d", &st[0][i]);   // 第 0 层直接读入,省掉 a[]

    lg2v[1] = 0;                                            // 长度 1 只需要第 0 层
    for (int i = 2; i <= n; ++i) lg2v[i] = lg2v[i >> 1] + 1;

    // 递推:长度 2^k 的区间 = 左右两个长度 2^(k-1) 的区间
    for (int k = 1; (1 << k) <= n; ++k)
        for (int i = 1; i + (1 << k) - 1 <= n; ++i)         // 右端点不越界
            st[k][i] = max(st[k - 1][i], st[k - 1][i + (1 << (k - 1))]);

    while (m--) {
        int l, r;
        scanf("%d %d", &l, &r);
        int k = lg2v[r - l + 1];                            // 2^k <= 长度 < 2^(k+1)
        // 左段以 l 开头,右段以 r 结尾;两段必然覆盖整个区间
        printf("%d\n", max(st[k][l], st[k][r - (1 << k) + 1]));
    }
    return 0;
}
逐行拆解
代码片段含义与理由
const int K = 17;层数上限。n ≤ 105 时 ⌊log2 n⌋ = 16,需要 0…16 共 17 层。少开一层就会写越界。
int st[K][N];层放第一维、起点放第二维。查询时同一个 k 连查两个起点,行内地址连续;写成 st[i][k] 会在两个不相邻的行之间来回跳。
scanf("%d %d", &n, &m);询问最多 2×106 次、整数最多 4×106 个,scanf 直读比 cin 少一层类型解析。
scanf("%d", &st[0][i])读进第 0 层。第 0 层就是原数组,这样省掉一个 a[],也省掉一次复制。
lg2v[1] = 0;长度 1 对应第 0 层。这个初值必须写,循环是从 2 开始的。
lg2v[i] = lg2v[i >> 1] + 1;i 的层号 = ⌊i/2⌋ 的层号 + 1。从小到大推,右式一定已算好。
(1 << k) <= n只要 2k ≤ n,这一层就存在合法区间。n = 105 时建到 k = 16 为止。
i + (1 << k) - 1 <= n区间右端点不能越过 n。缺这个条件的症状是越界写,小数据正常、大数据 RE。
st[k-1][i + (1 << (k-1))]右半段的左端点 = i + 2k-1。这是整个递推唯一容易写错的下标。
int k = lg2v[r - l + 1];长度换层号,一次数组访问,O(1)。写成 r − l 会取小一层,只覆盖大半个区间。
st[k][r - (1 << k) + 1]以 r 结尾、长度 2k 的那一段。+1 不能丢,丢了整段左移一格,答案可能取到区间外的数。
max(两段)重叠部分被算了两次,但 max 幂等,结果不变。换成 + 就会把重叠部分加两遍。
printf("%d\n", ...)2×106 行输出,用 "\n" 而不是 endl。endl 每次都刷新缓冲区,实测慢 3~10 倍。

ST 表是倍增在“数组上的一段”里的用法。同一个思想在树上是求 LCA:给定以 s 为根的树和两点 u、v,求它们最近的公共祖先。

朴素做法:从 u 一路往上走,把经过的点打上标记;再从 v 往上走,第一次踩到标记的点就是 LCA。单次 O(深度),5×105 次询问在一条链上直接退化成 2.5×1011 次操作。倍增把它压到 O(log n):只预存“往上跳 2k 步到哪”这 n log n 个位置。

新函数拆解 · 倍增跳表 up[k][v]
up[0][v] = fa[v]; up[k][v] = up[k-1][up[k-1][v]];
组成含义 / 格式要求
up[k][v]从 v 往上走 2k 步到达的点。k = 0 是父亲,k = 1 是爷爷,k = 2 是往上第 4 个点。
递推依据走 2k 步 = 先走 2k-1 步、再走 2k-1 步。所以两次 up[k-1] 串起来就是 up[k],一行搞定。
0 号哨兵把根的父亲设成 0,并让 up[*][0] = 0、dep[0] = 0。往上跳出树根时自动停在 0,全程不用特判。
计算顺序必须等 up[k-1][v] 的整行算完,才能算 up[k][v]。所以要么 BFS(父亲先出队),要么在 DFS 里“进儿子之前”把当前点整行算完。
深度差怎么用d = dep[u] − dep[v] 是个整数,按二进制位从低到高扫,第 k 位是 1 就 u = up[k][u],总共只有 O(log n) 跳,而且跳的步数加起来恰好是 d。
LCA 的第二步深度对齐后,从 k = K−1 往 0 试:若 up[k][u] ≠ up[k][v],说明还跳不到公共祖先之上,可以跳;循环结束时 u、v 必是 LCA 的两个不同儿子,答案是 up[0][u]。
最小可运行示例
示例
// 建表(跟 BFS 一起做,避免链状树爆栈)
void bfs(int root) {
    queue<int> q; q.push(root); dep[root] = 1;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int k = 1; k < K; ++k) up[k][u] = up[k-1][up[k-1][u]];
        for (int e = head[u]; e; e = nxt[e]) {
            int v = to[e];
            if (v == up[0][u]) continue;      // 跳过父亲
            up[0][v] = u; dep[v] = dep[u] + 1;
            q.push(v);
        }
    }
}

// up[k][3] 的含义:从 3 号点往上跳 2^k 步
// 例:路径 3 -> 5 -> 8 -> 12 -> 1 -> 0(0 是树外的哨兵)
//     up[0][3] = 5    往上第 1 步
//     up[1][3] = 8    往上第 2 步(= up[0][up[0][3]])
//     up[2][3] = 1    往上第 4 步(= up[1][up[1][3]] = up[1][8])
注意

数组开成 up[K][N]:K ≈ log2 n + 1。n = 5×105 时 K 取 20 足够。

用 BFS 建表而不是递归 DFS:链状树深度 5×105,每层栈帧几十字节,递归会爆栈(RE)。

倍增求 LCA(输入格式同 P3379:n、m、根,n−1 条边,m 次询问)
#include <bits/stdc++.h>
using namespace std;

const int N = 500005;                  // P3379 的量级:5×10^5 个点
const int K = 20;                      // 2^18 = 262144 <= 5e5 < 2^19,层数到 18,开 20 留余量

int head[N], nxt[N << 1], to[N << 1], ecnt;   // 链式前向星(第 41 节)
int up[K][N], dep[N];

void addEdge(int u, int v) {
    to[++ecnt] = v; nxt[ecnt] = head[u]; head[u] = ecnt;
}

void bfs(int root) {
    queue<int> q;
    q.push(root);
    dep[root] = 1;                     // 根的深度设 1,于是 dep[0] = 0 天然是哨兵
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int k = 1; k < K; ++k)    // 整行一次算完:2^k = 2^(k-1) + 2^(k-1)
            up[k][u] = up[k - 1][up[k - 1][u]];
        for (int e = head[u]; e; e = nxt[e]) {
            int v = to[e];
            if (v == up[0][u]) continue;         // 无向边存了两遍,跳过父亲
            up[0][v] = u;                        // 儿子的一级祖先就是 u
            dep[v] = dep[u] + 1;
            q.push(v);
        }
    }
}

int lca(int u, int v) {
    if (dep[u] < dep[v]) swap(u, v);   // 让 u 更深
    int d = dep[u] - dep[v];
    for (int k = 0; k < K; ++k)        // 深度差按二进制拆开,逐位往上跳
        if (d >> k & 1) u = up[k][u];
    if (u == v) return u;              // v 本来就是 u 的祖先
    for (int k = K - 1; k >= 0; --k)   // 从大到小试:跳完还不同就跳
        if (up[k][u] != up[k][v]) { u = up[k][u]; v = up[k][v]; }
    return up[0][u];                   // 此时 u、v 是 LCA 的两个儿子
}

int main() {
    int n, m, s;
    scanf("%d %d %d", &n, &m, &s);
    for (int i = 1; i < n; ++i) {
        int u, v; scanf("%d %d", &u, &v);
        addEdge(u, v); addEdge(v, u);
    }
    bfs(s);
    while (m--) {
        int a, b; scanf("%d %d", &a, &b);
        printf("%d\n", lca(a, b));
    }
    return 0;
}
LCA 代码的三处关键
代码片段含义与理由
dep[root] = 1;根深度设 1,则 dep[0] = 0。0 号点成了“树外”的哨兵,跳到根外面会自动停住,不用写 if。
up[k][u] = up[k-1][up[k-1][u]];内层先取 up[k-1][u],外层再取一次 up[k-1]。要求内层那一点的整行已经算好——BFS 里它一定是 u 的祖先,早就出队算完了。
if (v == up[0][u]) continue;无向边正反存了两份。漏了这一行会把父亲也当儿子压进队列,深度层层加错,最后死循环。
if (d >> k & 1) u = up[k][u];把深度差 d 拆成二进制。d 的每一位最多被用一次,跳完恰好等于 d,不会多跳一步。
if (u == v) return u;深度对齐后两点重合,说明 v 是 u 的祖先,答案就是 v。少了这句,下面会多跳一层,返回 v 的父亲。
for (int k = K - 1; k >= 0; --k)★ 必须从大到小。从大到小每次跳“跳了还不同”的最大步长,最后必然停在 LCA 下方;从小到大很可能第一步就跨过 LCA,再也回不来。
if (up[k][u] != up[k][v])判据是“跳完还不同”。相同说明这一步会落到 LCA 或 LCA 之上,不能跳。
return up[0][u];循环收敛时 u、v 深度相同且是 LCA 的两个不同儿子,父亲即答案。
这一节的 8 种典型错法(附症状)

WA,答案偏小。—— 查询写成 st[k][l] 与 st[k][l + 2^k - 1],右对齐的那一段没取,区间右半边根本没参与。第二个下标必须是 r - (1 << k) + 1。

WA,答案偏大。—— 把 max 换成 + 或 ×。两段在 len 不是 2 的幂时必然重叠,重叠部分被算了两次。

RE,或只有大数据错。—— 建表少了 i + (1 << k) - 1 <= n,st[k][i] 写到数组外面,踩坏 lg 数组或别的变量。

RE,段错误。—— K 开小一层。n = 105 时要用到第 16 层,写 K = 16 就越界;n = 5×105 的 LCA 要用到第 18 层,写 K = 18 同样越界。

TLE。—— m = 2×106 还开着 cin/cout 同步、每行用 endl。总量 4×106 次读写,光刷新缓冲区就能吃掉 1 秒以上。

WA,全负区间输出 0。—— 把答案初值写成 0 再 max 累加。区间里全是负数时 0 比任何元素都大。正确写法是直接对两个真实元素取 max,不设初值。

RE(爆栈)。—— 倍增 LCA 用递归 DFS 建表,遇到链状树(深度 5×105)栈溢出。改用 BFS。

WA,LCA 差一层。—— 第二步的循环写成 for (int k = 0; k < K; ++k)。从小到大跳会越过 LCA,返回的是 LCA 的某个祖先。

自己动手复现

关掉页面,把 P3865 从空文件敲一遍并提交。

改一改 ①:把查询里的 max 换成 +,输入上面的样例,手算 [1,6]:st[2][1] = 9(区间 1–4)、st[2][3] = 7(区间 3–6),相加得 16,而真实和是 27。亲眼看清“重叠”是怎么回事。

改一改 ②:删掉 lg 数组,查询里写 int k = (int)log2(r - l + 1);,构造长度 8、16 的询问,看会不会因为浮点误差取到 2 层而不是 3 层。

改一改 ③:把 st 声明改成 st[N][K](两个维度交换),代码逻辑不变,用 2×106 次询问计时对比——差的是缓存命中,不是算法。

改一改 ④:把 LCA 代码交到 P3379 上,再把第二步的循环方向改成从小到大,重新提交,看答案错成什么样。

数学补给 · log2、二进制分解与区间覆盖

⌊log2 n⌋ 就是 n 的二进制位数减 1。这个等式说明:倍增的层数,和整数的二进制长度是同一件事。估 K 的时候不用背表,把 n 写成二进制数位数即可。

两段必覆盖的严格论证:设 len = r − l + 1,k = ⌊log2 len⌋,于是 2k ≤ len < 2k+1。左段 L = [l, l+2k−1],右段 R = [r−2k+1, r]。两段的端点都落在 [l, r] 里,所以 L、R ⊆ [l, r]。若 L ∪ R 有缺口,则 l+2k−1 < r−2k+1,即 len = r−l+1 > 2k+1,与 k 的定义矛盾。所以 L ∪ R = [l, r],两段至多重叠 2k+1 − len 个元素。

幂等性为什么是必要条件:把运算记作 ⊕。要允许“同一元素被算两次”,必须 a ⊕ a = a(幂等);要能“劈半合并”,必须有结合律。max、min、gcd、lcm、按位或、按位与都满足这两条;加法、乘法、异或只满足结合律不满足幂等,所以 sum 的区间查询必须用前缀和(第 18 节)或线段树(第 47 节)。

空间账:ST 表存 (⌊log2 n⌋ + 1) × n 个 int。n = 105 时 17 × 105 × 4 B = 6.8 MB;lg 数组 0.4 MB。总计 7 MB 出头,洛谷常见的 125 MB 限制下非常宽裕;但 n 涨到 106 就要 20 层、80 MB,这时候要么改用分块(第 57 节),要么改用线段树把空间压回 O(n)。

LCA 的运算量:预处理 n log n ≈ 5×105 × 19 = 9.5×106 次赋值;单次询问第一步最多 19 次跳、第二步最多 20 次判断,合计不到 40 次比较,与树有多大完全无关。这就是“把 O(n) 压成 O(log n)”的实际含义:把和规模成正比的部分全部搬到预处理里。

举一反三 · 想清楚这些再往下走
  1. 如果询问换成“区间 gcd”或“区间按位或”,ST 表还能不能原样用?区间和为什么不行?请从“一个元素最多被算几次”这个角度回答。
  2. 要用 ST 表查“区间内出现次数最多的数”或者“区间第 k 小”,行不行?不行的根本原因是缺少哪条性质?
  3. 倍增还能用在哪?至少举出两个例子并说清“跳 2k 步”在那里对应什么操作(提示:快速幂的指数、树上第 k 级祖先、后缀数组的倍增构造)。
  4. 如果数组会被单点修改、同时要区间最大值,ST 表还能保持 O(1) 查询吗?为什么?这时候应该换成什么结构?
  5. 二维 ST 表求子矩阵最大值:多一维之后,预处理和查询各变成多少?空间涨到什么量级?
  6. LCA 的第二步为什么必须从大到小枚举 k?请构造一棵具体的小树,说明从小到大枚举会跳到哪里。
  7. 如果图的形状不是树而是 DAG(一个点可以有多个父亲),“LCA”还有意义吗?怎么定义才不矛盾?
自测:能全部回答再进下一节
挑战题 · Balanced Lineup G
洛谷 P2880普及/提高-去洛谷做这题 ↗

农夫约翰有 N 头奶牛,每天按固定顺序站成一排,第 i 头奶牛身高为 hi。有 Q 次询问,每次给出一个区间 [A, B],求这段区间里最高牛与最矮牛的身高差。

输入:第一行两个整数 N、Q(1 ≤ N ≤ 5×104,1 ≤ Q ≤ 2×105)。接下来 N 行,每行一个整数 hi(1 ≤ hi ≤ 106)。接下来 Q 行,每行两个整数 A、B(1 ≤ A ≤ B ≤ N)。

输出:Q 行,每行一个整数,表示 hA 到 hB 中的最大值减最小值。

引导性提示(先只看这里)

题目只要区间最值,没有任何修改操作——这类题的第一反应应该是 ST 表,而不是线段树。

一次询问要两个最值。同一套 st 数组能不能同时存最大和最小?如果不能,要开几个二维数组?

答案就是“区间最大值 − 区间最小值”。注意:局部变量的初值、数组的默认值会不会影响 min 的结果?

想看思路了?点开完整思路

把 ST 表模板复制两份:mx[k][i] 存区间 [i, i+2^k−1] 的最大值,mn[k][i] 存最小值,两者的建表方式完全一样,只是合并时一个用 max、一个用 min。

第 0 层就是原数组,mx[0][i] = mn[0][i] = h[i]。这一步是 min 表唯一能拿到真实值的地方,不能指望全局数组的 0 初值——hi ≥ 1,用 0 初始化会让所有 min 都变成 0。

每次询问按同样的 k 取出两段:hi = max(mx[k][A], mx[k][B−2^k+1]),lo = min(mn[k][A], mn[k][B−2^k+1]),输出 hi − lo。两段重叠对 max 和 min 都没有影响,所以同一个 k 可以直接用于两张表。

复杂度:预处理 O(N log N) ≈ 5×104 × 16 = 8×105,Q 次询问每次 4 次取最值,总计不到 106 次运算。

还是不会?点开题解代码与拆解
参考代码
#include <bits/stdc++.h>
using namespace std;

const int N = 50005;
const int K = 17;                       // 2^15 = 32768 <= 5e4 < 65536,层数最多到 15

int mx[K][N], mn[K][N];
int lg2v[N];

int main() {
    int n, q;
    scanf("%d %d", &n, &q);

    lg2v[1] = 0;
    for (int i = 2; i <= n; ++i) lg2v[i] = lg2v[i >> 1] + 1;

    for (int i = 1; i <= n; ++i) {
        int x; scanf("%d", &x);
        mx[0][i] = mn[0][i] = x;        // 长度 1 的区间,最大最小都是自己
    }

    for (int k = 1; (1 << k) <= n; ++k) {
        for (int i = 1; i + (1 << k) - 1 <= n; ++i) {
            int j = i + (1 << (k - 1));             // 右半段的左端点
            mx[k][i] = max(mx[k - 1][i], mx[k - 1][j]);
            mn[k][i] = min(mn[k - 1][i], mn[k - 1][j]);
        }
    }

    while (q--) {
        int a, b;
        scanf("%d %d", &a, &b);
        int k = lg2v[b - a + 1];
        int hi = max(mx[k][a], mx[k][b - (1 << k) + 1]);
        int lo = min(mn[k][a], mn[k][b - (1 << k) + 1]);
        printf("%d\n", hi - lo);         // 身高差,不是和
    }
    return 0;
}
关键片段拆解
片段作用
int mx[K][N], mn[K][N];两张表结构完全相同,只有合并运算不同。一次建表顺手把两个都填好,比建两遍各扫一次省一半时间。
mx[0][i] = mn[0][i] = x;★ 第 0 层必须真的写入。全局数组默认是 0,如果漏了 mn[0][i],min 会在第 1 层就把 0 卷进去,答案恒等于最大值。
(1 << k) <= n层数上限由 n 决定:215 = 32768 ≤ 5×104,所以最高建到 k = 15。
int j = i + (1 << (k - 1));右半段起点。写成 i + (1 << k) 会跳过半个区间,写成 i + (1 << k) - 1 会少覆盖一个元素。
int k = lg2v[b - a + 1];长度 → 层号。两个查询都复用这个 k,因为 mx、mn 的层划分完全一致。
b - (1 << k) + 1右段的起点。与模板一致,+1 不能丢。
hi - lo题目要的是差。写成 lo - hi 会输出负数;写成 hi + lo 是另一道题。
这题最容易错的地方
  • mn 表没有写第 0 层:全局数组的 0 被卷进 min,n 以内的最小值全变 0,输出恒等于最大值,样例就能看出来。
  • K 写成 15:n 到 5×104 时需要第 15 层,数组只有 0…14,越界写 → RE 或答案随机。
  • 两张表用了不同的 k:mx 取 lg2v[len]、mn 取 lg2v[len−1],覆盖区间不同,差值没有意义。
  • 把 hi − lo 写成 lo − hi 或 hi + lo:前者三个询问全是负数,后者全错,样例一眼可见,别靠样例蒙。
延伸阅读 / 参考