数论变换(NTT)
目录
- [为什么需要 NTT:从多项式乘法说起](#为什么需要 NTT:从多项式乘法说起)
- [从 FFT 到 NTT:动机与直觉](#从 FFT 到 NTT:动机与直觉)
- 数学基础:模运算、原根与单位根
- [NTT 核心算法:蝴蝶变换详解](#NTT 核心算法:蝴蝶变换详解)
- 完整代码实现
- [关键技巧:模数选择与任意模数 NTT](#关键技巧:模数选择与任意模数 NTT)
- 复杂度分析
- 实际应用场景
- 知识全景图(思维导图)
- 总结
1. 为什么需要 NTT:从多项式乘法说起
假设我们有两个多项式:
A ( x ) = a 0 + a 1 x + a 2 x 2 + ⋯ + a n − 1 x n − 1 A(x) = a_0 + a_1x + a_2x^2 + \cdots + a_{n-1}x^{n-1} A(x)=a0+a1x+a2x2+⋯+an−1xn−1
B ( x ) = b 0 + b 1 x + b 2 x 2 + ⋯ + b n − 1 x n − 1 B(x) = b_0 + b_1x + b_2x^2 + \cdots + b_{n-1}x^{n-1} B(x)=b0+b1x+b2x2+⋯+bn−1xn−1
它们的乘积 C ( x ) = A ( x ) ⋅ B ( x ) C(x) = A(x) \cdot B(x) C(x)=A(x)⋅B(x) 的系数为卷积:
c k = ∑ i + j = k a i b j c_k = \sum_{i+j=k} a_i b_j ck=i+j=k∑aibj
如果直接按定义计算,复杂度是 O ( n 2 ) O(n^2) O(n2)。当 n n n 达到 10 5 10^5 105 甚至 10 6 10^6 106 级别时,这个复杂度是无法接受的。
关键洞察:多项式有两种等价表示方式------
- 系数表示 : ( a 0 , a 1 , ... , a n − 1 ) (a_0, a_1, \dots, a_{n-1}) (a0,a1,...,an−1)
- 点值表示 :在 n n n 个不同点上的取值 ( A ( x 0 ) , A ( x 1 ) , ... , A ( x n − 1 ) ) (A(x_0), A(x_1), \dots, A(x_{n-1})) (A(x0),A(x1),...,A(xn−1))
在点值表示下,两个多项式相乘只需要逐点相乘 ,复杂度是 O ( n ) O(n) O(n)!
C ( x i ) = A ( x i ) ⋅ B ( x i ) C(x_i) = A(x_i) \cdot B(x_i) C(xi)=A(xi)⋅B(xi)
于是问题转化为:如何快速地在"系数表示"与"点值表示"之间转换?这正是 FFT/NTT 要解决的问题。
系数表示 A, B --[快速变换]--> 点值表示 A(x_i), B(x_i)
|
逐点相乘 O(n)
|
系数表示 C <--[快速逆变换]-- 点值表示 C(x_i)
2. 从 FFT 到 NTT:动机与直觉
FFT(快速傅里叶变换)选取的求值点是复数单位根 ω n = e 2 π i / n \omega_n = e^{2\pi i/n} ωn=e2πi/n,利用其优美的对称性质,把 O ( n 2 ) O(n^2) O(n2) 的求值过程降到 O ( n log n ) O(n\log n) O(nlogn)。
但 FFT 存在两个现实问题:
- 浮点误差 :复数运算涉及三角函数与浮点数,当 n n n 很大或系数很大时,精度损失会导致结果出错。
- 无法直接用于模意义下的精确计算:很多场景(如竞赛、密码学)要求结果对某个质数取模,FFT 的浮点误差在这里是不可接受的。
NTT 的思路 :把复数单位根替换成"模意义下的单位根",所有运算都在有限域 Z p \mathbb{Z}_p Zp( p p p 为质数)中进行整数运算,从而做到完全精确。
这就是 NTT 的核心动机------用有限域上的代数结构,模拟复数域上 FFT 的对称性。
3. 数学基础:模运算、原根与单位根
3.1 为什么选质数模数
NTT 要求在模 p p p 意义下存在类似"单位根"的元素,这依赖于原根的存在性,而原根的存在性与模数的乘法群结构密切相关。为方便起见,通常选择:
p = c ⋅ 2 k + 1 p = c \cdot 2^k + 1 p=c⋅2k+1
这类质数的乘法群 Z p ∗ \mathbb{Z}_p^* Zp∗ 阶为 p − 1 = c ⋅ 2 k p-1 = c \cdot 2^k p−1=c⋅2k,包含足够大的 2 的幂次因子,能支持长度为 2 k 2^k 2k 的变换。
常用的 NTT 友好模数:
| 模数 p p p | 分解形式 | 原根 g g g |
|---|---|---|
| 998244353 | 119 × 2 23 + 1 119 \times 2^{23} + 1 119×223+1 | 3 |
| 1004535809 | 479 × 2 21 + 1 479 \times 2^{21} + 1 479×221+1 | 3 |
| 469762049 | 7 × 2 26 + 1 7 \times 2^{26} + 1 7×226+1 | 3 |
3.2 原根(Primitive Root)
若 g g g 满足其在模 p p p 下的阶 恰好为 p − 1 p-1 p−1(即 g p − 1 ≡ 1 g^{p-1} \equiv 1 gp−1≡1,且没有更小的正整数指数使其成立),则称 g g g 为模 p p p 的一个原根。
原根的存在性由数论定理保证:每个质数 p p p 都存在原根。
求原根的常规方法:
- 分解 p − 1 p-1 p−1 的所有质因子 q 1 , q 2 , ... , q m q_1, q_2, \dots, q_m q1,q2,...,qm
- 枚举候选 g = 2 , 3 , 4 , ... g = 2, 3, 4, \dots g=2,3,4,...
- 检验对每个 q i q_i qi,是否满足 g ( p − 1 ) / q i ≢ 1 ( m o d p ) g^{(p-1)/q_i} \not\equiv 1 \pmod p g(p−1)/qi≡1(modp)
- 若全部满足,则 g g g 是原根
3.3 n 次单位根
有了原根 g g g,我们定义模 p p p 下的 n n n 次单位根:
ω n = g p − 1 n m o d p \omega_n = g^{\frac{p-1}{n}} \bmod p ωn=gnp−1modp
它满足与复数单位根完全类似的性质:
- 周期性 : ω n n ≡ 1 ( m o d p ) \omega_n^n \equiv 1 \pmod p ωnn≡1(modp)
- 消去性质 : ω d n d k = ω n k \omega_{dn}^{dk} = \omega_n^k ωdndk=ωnk
- 折半性质 : ω n n / 2 ≡ − 1 ( m o d p ) \omega_n^{n/2} \equiv -1 \pmod p ωnn/2≡−1(modp)(这是分治合并的关键)
- 单位根反演 : ∑ k = 0 n − 1 ω n i k = { n i ≡ 0 ( m o d n ) 0 otherwise \sum_{k=0}^{n-1} \omega_n^{ik} = \begin{cases} n & i \equiv 0 \pmod n \\ 0 & \text{otherwise} \end{cases} ∑k=0n−1ωnik={n0i≡0(modn)otherwise
这些性质与 FFT 中复数单位根完全对应,正是它们保证了分治算法可以照搬到 NTT 上。
4. NTT 核心算法:蝴蝶变换详解
4.1 分治思想(Cooley-Tukey)
设多项式次数为 n n n( n n n 为 2 的幂),将其按下标奇偶拆分:
A ( x ) = A 0 ( x 2 ) + x ⋅ A 1 ( x 2 ) A(x) = A_0(x^2) + x \cdot A_1(x^2) A(x)=A0(x2)+x⋅A1(x2)
其中 A 0 A_0 A0 由偶数项组成, A 1 A_1 A1 由奇数项组成。
对于单位根 ω n k \omega_n^k ωnk 与 ω n k + n / 2 \omega_n^{k+n/2} ωnk+n/2,利用折半性质:
A ( ω n k ) = A 0 ( ω n / 2 k ) + ω n k ⋅ A 1 ( ω n / 2 k ) A(\omega_n^k) = A_0(\omega_{n/2}^k) + \omega_n^k \cdot A_1(\omega_{n/2}^k) A(ωnk)=A0(ωn/2k)+ωnk⋅A1(ωn/2k)
A ( ω n k + n / 2 ) = A 0 ( ω n / 2 k ) − ω n k ⋅ A 1 ( ω n / 2 k ) A(\omega_n^{k+n/2}) = A_0(\omega_{n/2}^k) - \omega_n^k \cdot A_1(\omega_{n/2}^k) A(ωnk+n/2)=A0(ωn/2k)−ωnk⋅A1(ωn/2k)
这一对公式就是蝴蝶操作(Butterfly Operation) :只需计算一次 A 0 ( ω n / 2 k ) A_0(\omega_{n/2}^k) A0(ωn/2k) 和 A 1 ( ω n / 2 k ) A_1(\omega_{n/2}^k) A1(ωn/2k) 的乘积项,就能同时得到两个位置的结果。
A0(ω^k) ──────┬────────► A(ω^k) = A0 + ω^k · A1
│
A1(ω^k)──[×ω^k]┴────────► A(ω^(k+n/2)) = A0 - ω^k · A1
这就是"蝴蝶"这个名字的来源------图形状如蝴蝶展翅。
4.2 位逆序置换(Bit-Reversal)
递归分治的迭代实现中,需要先将数组按二进制位逆序 重新排列。例如 n = 8 n=8 n=8 时:
| 原下标(二进制) | 逆序后 | 十进制 |
|---|---|---|
| 000 | 000 | 0 |
| 001 | 100 | 4 |
| 010 | 010 | 2 |
| 011 | 110 | 6 |
| 100 | 001 | 1 |
| 101 | 101 | 5 |
| 110 | 011 | 3 |
| 111 | 111 | 7 |
这样排列后,就可以从底层(步长为1)开始自底向上迭代合并,避免递归带来的函数调用开销。
4.3 逆变换 INTT
逆变换的公式与正变换几乎一致,只需将单位根替换为其逆元 ω n − 1 \omega_n^{-1} ωn−1,并在最后将每个结果除以 n n n(即乘以 n − 1 m o d p n^{-1} \bmod p n−1modp):
a i = 1 n ∑ k = 0 n − 1 A ( ω n − k ) ⋅ ω n − i k a_i = \frac{1}{n} \sum_{k=0}^{n-1} A(\omega_n^{-k}) \cdot \omega_n^{-ik} ai=n1k=0∑n−1A(ωn−k)⋅ωn−ik
5. 完整代码实现
以下是一份清晰完整的 C++ 实现(迭代版,998244353 为模数):
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll MOD = 998244353;
const ll G = 3; // 原根
ll qpow(ll a, ll b, ll mod) {
ll res = 1; a %= mod;
while (b > 0) {
if (b & 1) res = res * a % mod;
a = a * a % mod;
b >>= 1;
}
return res;
}
void ntt(vector<ll>& a, bool invert) {
int n = a.size();
// 位逆序置换
for (int i = 1, j = 0; i < n; i++) {
int bit = n >> 1;
for (; j & bit; bit >>= 1) j ^= bit;
j ^= bit;
if (i < j) swap(a[i], a[j]);
}
// 蝴蝶合并
for (int len = 2; len <= n; len <<= 1) {
ll w = qpow(G, (MOD - 1) / len, MOD);
if (invert) w = qpow(w, MOD - 2, MOD); // 逆变换用逆元
for (int i = 0; i < n; i += len) {
ll wn = 1;
for (int j = 0; j < len / 2; j++) {
ll u = a[i + j];
ll v = a[i + j + len / 2] * wn % MOD;
a[i + j] = (u + v) % MOD;
a[i + j + len / 2] = (u - v + MOD) % MOD;
wn = wn * w % MOD;
}
}
}
if (invert) {
ll n_inv = qpow(n, MOD - 2, MOD);
for (ll& x : a) x = x * n_inv % MOD;
}
}
vector<ll> multiply(vector<ll> a, vector<ll> b) {
int sz = 1;
while (sz < (int)(a.size() + b.size())) sz <<= 1;
a.resize(sz); b.resize(sz);
ntt(a, false);
ntt(b, false);
for (int i = 0; i < sz; i++) a[i] = a[i] * b[i] % MOD;
ntt(a, true);
return a;
}
使用示例 :计算 ( 1 + 2 x ) × ( 3 + 4 x ) (1+2x) \times (3+4x) (1+2x)×(3+4x)
cpp
int main() {
vector<ll> A = {1, 2};
vector<ll> B = {3, 4};
vector<ll> C = multiply(A, B);
// 期望结果:3 + 10x + 8x^2
for (int i = 0; i < 3; i++) cout << C[i] << " ";
}
6. 关键技巧:模数选择与任意模数 NTT
6.1 为什么不能任选模数
标准 NTT 要求模数 p p p 满足 p − 1 p-1 p−1 含有足够大的 2 的幂次因子。如果题目要求对任意模数 (比如 10 9 + 7 10^9+7 109+7,虽然它恰好可用,但很多模数如 10 6 + 3 10^6+3 106+3 并不满足条件)取模,就需要额外技巧。
6.2 三模数 NTT + CRT
思路 :选取三个 NTT 友好的大质数 p 1 , p 2 , p 3 p_1, p_2, p_3 p1,p2,p3(乘积远超过结果的最大可能值),分别在这三个模数下做 NTT 卷积,得到三组结果,再用**中国剩余定理(CRT)**合并出真实结果(此时结果是一个不超过 p 1 p 2 p 3 p_1p_2p_3 p1p2p3 的大整数),最后对目标模数取模。
原始数据 ──┬──► NTT (mod p1) ──► 结果1
├──► NTT (mod p2) ──► 结果2
└──► NTT (mod p3) ──► 结果3
│
CRT合并 ──► 真实大整数结果
│
对目标模数取模 ──► 最终答案
6.3 MTT(拆系数)方法
另一种思路是把每个系数拆成高位和低位两部分(例如按 p \sqrt{p} p 拆分),转化为若干次可以在单一模数下完成的 NTT,从而避免使用多模数 CRT,代码更简洁但常数稍大。
7. 复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 暴力卷积 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) |
| FFT | O ( n log n ) O(n\log n) O(nlogn) | O ( n ) O(n) O(n) |
| NTT | O ( n log n ) O(n\log n) O(nlogn) | O ( n ) O(n) O(n) |
| 三模数NTT+CRT | O ( n log n ) O(n\log n) O(nlogn)(常数为3倍) | O ( n ) O(n) O(n) |
NTT 相比 FFT,虽然渐进复杂度相同,但由于是纯整数运算(模乘、模加),在很多场景下常数因子更优,且完全没有精度问题。
8. 实际应用场景
8.1 大整数乘法(高精度乘法)
将大整数看作多项式(每一位是一个系数),乘法转化为多项式乘法,利用 NTT 加速,可以把高精度乘法从 O ( n 2 ) O(n^2) O(n2) 降到 O ( n log n ) O(n\log n) O(nlogn),这是 Java BigInteger、Python 大整数乘法等底层实现常用的手段(结合 Karatsuba/Toom-Cook 用于中等规模,NTT/FFT 用于超大规模)。
8.2 多项式相关运算
NTT 是现代多项式算法的基石,可以进一步实现:
- 多项式求逆:利用 Newton 迭代 + NTT 加速
- 多项式开根、exp、ln:竞赛中"多项式全家桶"的核心工具
- 多点求值与快速插值:结合分治 NTT
8.3 组合数学与生成函数
很多组合计数问题可以转化为生成函数的卷积运算,例如背包类 DP 的优化、EGF(指数生成函数)的合并等,都依赖 NTT 加速卷积过程。
8.4 字符串算法
某些字符串匹配、编辑距离类问题可以转化为卷积形式(如带通配符的字符串匹配),借助 NTT 在 O ( n log n ) O(n\log n) O(nlogn) 内求解。
8.5 密码学:格密码与同态加密
在现代密码学中,尤其是格密码(Lattice-based Cryptography)和同态加密领域(如 NIST 后量子标准 Kyber、Dilithium 等),底层运算大量依赖多项式环上的乘法,NTT 是这些方案实现高效运算的核心技术,直接影响加解密速度。
9. 知识全景图(思维导图)
为了方便你梳理 NTT 的整体知识体系,我整理了一份思维导图(见左侧/上方 Artifact 面板),涵盖从数学基础到应用场景的完整脉络:
- 起源与动机:多项式乘法、FFT 局限、NTT 优势
- 数学基础:模运算、原根、单位根性质
- 核心算法:蝴蝶变换、位逆序、递归/迭代实现、逆变换
- 关键技巧:模数选择、任意模数 NTT、优化手段
- 应用场景:大整数乘法、多项式运算、组合数学、字符串算法、密码学
- 复杂度分析:与暴力法、FFT 的对比
建议按照"是什么 → 为什么 → 怎么做 → 用在哪"的顺序阅读,逐步建立完整认知。
10. 总结
NTT 本质上是FFT 在有限域上的一个"整数版实现" :它借用了原根构造出的单位根,完美复刻了复数单位根的对称性质,从而把 O ( n 2 ) O(n^2) O(n2) 的多项式卷积降到 O ( n log n ) O(n\log n) O(nlogn),同时规避了浮点误差,实现了模意义下的精确计算。
掌握 NTT 需要理解三个层次:
- 数学层:为什么原根能扮演单位根的角色,其背后是有限域乘法群的循环结构
- 算法层 :蝴蝶变换如何通过分治把求值过程从 O ( n 2 ) O(n^2) O(n2) 降到 O ( n log n ) O(n\log n) O(nlogn)
- 工程层:如何应对模数限制(CRT、拆系数),如何进一步优化常数
无论是算法竞赛、大数运算库开发,还是前沿的后量子密码学,NTT 都是一个绕不开的核心工具。希望这篇文章能帮你建立起清晰而扎实的理解。
、