第63章 倍增法

第63章 倍增法

倍增法(Binary Lifting)是一种基于"成倍增长"思想的高效算法,通过预处理将问题分解为多个22的幂次规模的子问题,从而在查询时实现快速合并求解。该算法在时间复杂度上通常能将线性查询优化为对数级,广泛应用于数论(快速幂)、图论(树上倍增求祖先/LCA)、RMQ(ST 表)等领域。

倍增法:以 2 的幂次跳跃

63.1 倍增法的基本原理

63.1.1 核心思想

倍增法的核心是预处理出规模为20,21,22,...,2k2^{0},2^{1},2^{2},...,2^{k}的子问题的解,在查询时将原问题分解为若干个2的幂次对应的子问题,通过组合子问题的解得到原问题的答案。其本质是利用二进制表示的特性,将任意整数表示为若干个不重复的2的幂次之和,从而实现高效的分解与合并。

63.1.2 预处理与查询流程

预处理阶段:

  1. 确定需要预处理的最大幂次kk(kk为满足2k≤n2^{k} ≤n的最大整数,即k=log⁡2nk=\log_{2} n)。
  2. 对于每个位置ii,预处理出以ii为起点、长度为2j2^{j}区间的信息,记为f[i][j]f[i][j]。
  3. 预处理递推关系:
f[i][j]=combine(f[i][j−1], f[i+2j−1][j−1])f[i][j] = combine(f[i][j-1],\ f[i+2^{j-1}][j-1])

其中combinecombine为合并两个子问题解的函数。

查询阶段:

  1. 将查询规模mm拆解为二进制:m=2a+2b+⋯+2cm=2^{a}+2^{b}+\dots+2^{c}
  2. 依次处理每一个2的幂次子区间,通过combinecombine函数合并结果得到最终答案。

63.1.3 时间复杂度

  • 预处理:O(nlog⁡n)O(n \log n)
  • 单次查询:O(log⁡n)O(\log n)
为什么偏偏是"2 的幂次"?

因为任何正整数都能唯一表示为若干个不重复的 2 的幂次之和(二进制)。预处理出"长度为 1、2、4、8…"的子问题解后,任意长度都能被这些 2 的幂次"拼"出来,且拼出的段数 ≤ log⁡n\log n,从而把查询降到对数级。

63.2 倍增法的典型应用:快速幂

快速幂是倍增法在数论中的经典应用,用于高效计算an mod ma^{n} \bmod m,避免朴素循环O(n)O(n)的时间开销。

原理: 将指数nn拆分为二进制,例如n=5n=5二进制为101101,等价22+202^{2}+2^{0},则a5=a4×a1a^{5}=a^{4} × a^{1}。 提前递推a20,a21,a22...a^{2^{0}},a^{2^{1}},a^{2^{2}}...,遍历二进制位,若当前位为1则累乘对应幂次,全程取模防止溢出。

实现代码

long long fastPower (long long a, long long n, long long m) {
long long result=1;
a %= m;
while (n>0){
if(n & 1){
result = (result * a) % m;
}
a = (a * a) % m;
n >>= 1;
}
return result;
}

## 63.3 真题示例(ST 表:区间最值 RMQ)

> **【GESP 八级风格·编程题】**
> **题目描述**:给定一个长度为 $$n$$ 的数列,之后有 $$q$$ 次询问,每次询问区间 $$[l, r]$$ 中的最大值。$$n, q \le 10^5$$。
> **输入格式**:第一行 $$n$$;第二行 $$n$$ 个整数;第三行 $$q$$;之后 $$q$$ 行每行 $$l, r$$(下标从 0 开始)。
> **输出格式**:每个询问输出一个最大值。

**思路**:用倍增预处理出"长度为 $$2^j$$ 的区间最值",查询时把 $$[l,r]$$ 拆成两段已预处理的长度 $$2^k$$ 区间,取 max 即可,单次查询 $$O(1)$$。

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

const int MAXN = 100005;
int a[MAXN];
int st[MAXN][20]; // st[i][j] = 区间 [i, i + 2^j - 1] 的最大值
int n;

void build() {
for (int i = 0; i < n; ++i) st[i][0] = a[i]; // 长度 1 = 本身
for (int j = 1; (1 << j) <= n; ++j) // 长度 2^j
for (int i = 0; i + (1 << j) - 1 < n; ++i)
st[i][j] = max(st[i][j - 1],
st[i + (1 << (j - 1))][j - 1]); // 左右两半合并
}

int query(int l, int r) {
int k = (int)log2(r - l + 1); // 最大的 2^k ≤ 区间长度
return max(st[l][k], st[r - (1 << k) + 1][k]); // 两段覆盖整个区间
}

int main() {
cin >> n;
for (int i = 0; i < n; ++i) cin >> a[i];
build();
int q; cin >> q;
while (q--) {
int l, r; cin >> l >> r;
cout << query(l, r) << "\n";
}
return 0;
}
ST 表 vs 线段树
  • ST 表:预处理 O(nlog⁡n)O(n \log n),查询 O(1)O(1),但只能处理可重复贡献的问题(max/min/gcd),不支持单点修改;
  • 若需要动态修改区间,要用线段树/树状数组(查询 O(log⁡n)O(\log n))。 八级阶段,RMQ 静态最值优先掌握 ST 表。

练习题

说明

下列选择题考查倍增法、快速幂与 ST 表,参考答案默认折叠,点击"参考答案"展开。

1.(复杂度) 用倍增法(如 ST 表)预处理长度为 nn 的数组,时间复杂度是?

  • A. O(n)O(n)
  • B. O(nlog⁡n)O(n \log n)
  • C. O(n2)O(n^2)
  • D. O(log⁡n)O(\log n)
参考答案

B。倍增预处理要填 st[i][j](ii 有 nn 种,jj 有 log⁡n\log n 种),故 O(nlog⁡n)O(n \log n)。

2.(快速幂) 计算 an mod ma^n \bmod m 的快速幂算法,时间复杂度是?

  • A. O(n)O(n)
  • B. O(log⁡n)O(\log n)
  • C. O(n2)O(n^2)
  • D. O(1)O(1)
参考答案

B。快速幂把指数 nn 按二进制拆分,循环次数 = nn 的二进制位数 = ⌊log⁡2n⌋+1\lfloor \log_2 n \rfloor + 1,即 O(log⁡n)O(\log n)。

3.(快速幂细节) 快速幂代码中 if (n & 1) 的作用是?

  • A. 判断 nn 是否为 0
  • B. 判断当前二进制位是否为 1,是则把对应幂次乘进结果
  • C. 判断结果为奇数
  • D. 用于取模
参考答案

B。n & 1 取 nn 的最低二进制位;若该位为 1,说明当前这一"2 的幂次"需要乘入结果,随后 n >>= 1 处理下一位。

4.(ST 表) 关于 ST 表(倍增区间最值),下列说法正确的是?

  • A. 支持区间修改
  • B. 单次查询是 O(1)O(1)
  • C. 只能求区间和
  • D. 预处理是 O(n)O(n)
参考答案

B。ST 表查询 O(1)O(1)(两段预处理的 max 取较大);它不支持修改、只适合可重复贡献运算(max/min/gcd),预处理是 O(nlog⁡n)O(n\log n)。

5.(思想) 倍增法的核心思想是?

  • A. 每次把问题规模减半
  • B. 把任意规模拆成若干个 2 的幂次子问题快速合并
  • C. 用堆维护最优解
  • D. 分治排序
参考答案

B。倍增的本质是"二进制拆分 + 预处理 2 的幂次子问题",从而把查询降到对数级。A 描述的是二分/分治的某一种,不如 B 准确。

SShowball OI知识手册