倍增只有一个动作:先把“走 2k 步之后到哪”全部算出来,再把任意步数拆成二进制去拼。正整数的二进制分解唯一,所以 105 步最多拆成 17 段,每段长度都是 2 的幂。这一节用它做两件事:O(1) 的静态区间最值(ST 表)、O(log n) 的树上 LCA。
| 要做的事 | 数据规模 | 做法 | 复杂度 |
|---|---|---|---|
| 静态区间最值(ST 表) | n ≤ 105,m ≤ 2×106 | 倍增预处理 2 的幂长度区间 | 建表 O(n log n),单次 O(1) |
| 树上两点 LCA(倍增) | n, m ≤ 5×105 | up[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[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 或答案错,而且往往小样例看不出来。 |
为什么不用 log2 | log2 是浮点函数,一次调用几十纳秒,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[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 次操作上就是过与不过的差别。
给定 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 行,每行一个整数,表示对应询问的答案。
先估暴力:每次询问扫一遍 [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..] | 说明 |
|---|---|---|---|
| 0 | 1 | 9 3 1 7 5 2 6 4 | 原数组,一个数一段 |
| 1 | 2 | 9 3 7 7 5 6 6 | 每两个取 max |
| 2 | 4 | 9 7 7 7 6 | 每四个取 max |
| 3 | 8 | 9 | 整个数组 |
走一遍询问 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] | 从 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)。
#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;
}| 代码片段 | 含义与理由 |
|---|---|
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 的两个不同儿子,父亲即答案。 |
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 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)”的实际含义:把和规模成正比的部分全部搬到预处理里。
农夫约翰有 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 是另一道题。 |