多项式乘法是一个很常见重要的问题。给两个多项式
那么它们的乘积 的第 项系数是
这就是序列 和 的卷积。直接按定义算,需要枚举所有 ,复杂度是 。假定 规模相当,这个复杂度就是平方的,很快就不够用了。
大整数乘法也是同一个问题。把一个整数按进制 拆成若干位:
那么 在处理进位之前,第 位的原始值就是 。也就是说,多项式乘法、卷积、大整数乘法在核心计算上是同一个结构,只是最后解释结果的方式不同:多项式保留系数,卷积保留序列,大整数还要做一遍进位。

系数表示和点值表示
一个次数小于 的多项式,可以用它的 个系数表示:
这叫系数表示。它很适合做加法,因为对应系数相加即可;但相乘两个多项式会变成卷积,比较麻烦。
另一种表示方式是点值表示。选 个不同的点 ,记录
这些点值也可以唯一确定多项式。用点值表示做乘法非常轻松:如果 ,那么

所以只要逐点相乘就行。因此要避开平方的卷积,就要把系数表示转化成点值表示,进行点乘运算,然后把点值表示还原成系数表示。那么如何在系数表示和点值表示之间快速转换?
FFT 快速傅里叶变换
朴素求值要对每个点代入一次多项式,求一次是 ,一共 个点就是 。而 FFT 的关键是选择一组非常特殊的点,使求值在 内完成。
FFT 使用复数域里的单位根。长度为 时,取

取用它是因为他有一个性质: ,并且 两两不同。对一个系数序列 ,它的离散傅里叶变换可以写成
从信号与系统里傅里叶变换的意义来说,把 转化成 是从时域到频域的转换,而从多项式的角度来说,这其实就是把多项式 分别求解 。相比随便选 个点求解来说,求解这些特殊点的值,可以利用其数学性质加速。
蝶形变换
单位根有很强的对称性。假设 是 2 的整次幂,把多项式按偶数次和奇数次拆开:
其中
因此
而 也是一个单位根,所以可以递归求解。
设 ,那么原多项式在两个对应点上的值是
这就是蝶形合并。它说明,要算 ,可以先分别算偶数部分和奇数部分在 上的值,再用加减和乘以 合并。一次拆分把长度为 的问题变成两个长度为 的问题,每层合并花 ,一共有 层,所以整体是 。在迭代版 FFT 中,通常先做 bit-reversal 重排,然后从长度 的小块开始合并,块长依次翻倍。每一层会遍历所有块,每个块里做若干个蝶形变换。

逆变换
正变换把系数变成点值,逆变换把点值插值回系数。DFT 的逆变换形式是
也就是说,逆变换和正变换几乎一样,只是把 换成 ,最后所有结果再除以 。这个形式在 NTT 里也会原样保留,只是“除以 ”会变成乘上 在模意义下的逆元。
用 FFT 做卷积的流程很短:先把两个序列补零到长度 ,其中 至少是 ,通常取不小于它的最小二次幂。然后分别 FFT,逐点相乘,再逆 FFT。最终前 项就是卷积结果。
从 FFT 到 NTT 数论变换
FFT 用复数,速度快,但有浮点误差。对于整数卷积,尤其是竞赛、密码学或需要完全精确的场景,更常用 NTT。NTT 可以看作“把 FFT 搬到模质数意义下”。
现在,在模 的有限域里,如果存在一个元素 ,它的幂能生成所有非零元素,那么 是模 的原根。换句话说,,刚好遍历了 ,而 再次回到 。比如 是模 的一个原根(),但 不是。
由于模质数 下的非零元素有 个,我们想做长度为 的 NTT,就需要 整除 。
换一个稍大的例子。取 ,可以验证 是一个原根。现在想做长度为 的 NTT,可以构造
验证一下 ,并且在 次幂时都不会提前变成 。
这时我们就发现, 就可以扮演 FFT 里 的角色。同理, 是一个长度 16 的 NTT 单位根, 是一个长度 4 的 NTT 单位根。

更普遍的来说,若 是模 的原根,则
就是一个 阶单位根。这样,FFT 里关于单位根的推导仍然成立,只是所有加法、减法、乘法都放在模 下。复数单位根 变成模意义下的单位根 ;
在逆变换里,原理也完全相同,除以 变成乘 (乘法逆元);
常用模数 998244353
很常用,首先他是质数,并且
它足够大,最多可以生成长度 的 NTT 单位根。对多数算法题来说,这个长度已经足够大。同时这个数的大小刚好满足 不超过 int32 的上限, 没有超过 int64 的上限,利于实际实现。 是他的一个原根。
通用情况
那么更大的情况呢?和 FFT 的区别是,NTT 下,卷积系数的运算也在模 意义下进行,若系数最终没有超过 就好说,但如果超过,就会在取模意义下绕回来,我们这时无法得知算出的 实际上是 还是 。
假设输入系数非负,长度为 ,每个系数不超过 ,那么卷积中单个系数的上界大约是 。如果这个值可能超过模数,就要么选择更大的可用模数,要么使用多个 NTT 质数分别计算一次,再通过中国剩余定理合并。
具体来说,选几个互质模数 ,分别算出
只要真实的 小于 ,那么用 CRT 就可以唯一恢复 。实际工程里常见做法是使用多个 NTT-friendly prime,例如 、、 。
对于大整数乘法,还可以通过控制进制来降低单个卷积系数的上界。例如把十进制字符串拆成 或 进制,而不是直接用 进制。这个情况下,进制越大,卷积长度越短,但中间系数越容易溢出模数;进制越小,长度更长,但结果更安全。这是一个很实际的 trade-off。
#include using namespace std; const int MOD = 998244353; const int G = 3; long long mod_pow(long long a, long long e) { long long r = 1; while (e) { if (e & 1) r = r * a % MOD; a = a * a % MOD; e >>= 1; } return r; } void ntt(vector<int>& a, bool invert) { int n = (int)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) { int wlen = (int)mod_pow(G, (MOD - 1) / len); if (invert) wlen = (int)mod_pow(wlen, MOD - 2); for (int i = 0; i < n; i += len) { long long w = 1; int half = len >> 1; for (int j = 0; j < half; j++) { int u = a[i + j]; int v = (int)(a[i + j + half] * w % MOD); int x = u + v; if (x >= MOD) x -= MOD; int y = u - v; if (y < 0) y += MOD; a[i + j] = x; a[i + j + half] = y; w = w * wlen % MOD; } } } if (invert) { int inv_n = (int)mod_pow(n, MOD - 2); for (int& x : a) { x = (int)(1LL * x * inv_n % MOD); } } } vector<int> multiply(vector<int> a, vector<int> b) { if (a.empty() || b.empty()) return {}; int need = (int)a.size() + (int)b.size() - 1; int n = 1; while (n < need) n <<= 1; if ((MOD - 1) % n != 0) { throw runtime_error("NTT length is not supported by this modulus"); } a.resize(n); b.resize(n); ntt(a, false); ntt(b, false); for (int i = 0; i < n; i++) { a[i] = (int)(1LL * a[i] * b[i] % MOD); } ntt(a, true); a.resize(need); return a; }
这份实现使用迭代版 NTT(更快)。先用 bit-reversal 把数组重排成递归到底后的顺序,然后令块长 len 从 开始不断翻倍。每一层把两个长度为 len / 2 的相邻块合并成一个长度为 len 的块。
在一个块内,第 j 个蝶形变换取
然后写回
这里的 是当前块长对应的单位根。正变换使用 ,逆变换使用 。因此代码虽然没有显式递归,但它执行的合并顺序和递归 FFT/NTT 是一样的,只是把递归树按层展开了。

复杂度
为了方便,一般都会将两个初始序列长度相加,再补齐到最近的 2 的整数幂(这最多是原来的两倍长度)。设补零后的长度为 。直接卷积需要 ,FFT 或 NTT 的流程是两次正变换、一次逐点相乘、一次逆变换。正变换和逆变换的复杂度都是
空间复杂度通常是 。迭代实现会原地完成变换,除了输入数组和少量临时变量,不需要递归栈和额外的大数组。
在实际使用中,FFT 和 NTT 的选择并不是单纯的速度问题。FFT 可以处理更自由的长度和数值范围,但要处理浮点误差;NTT 结果精确,适合整数和模意义下的卷积,但受模数和可用变换长度限制。对于程序员来说,最常见的路径是:需要模卷积时优先 NTT;需要精确整数卷积且结果可能很大时,用多模 NTT 加 CRT;只需要近似或能容忍舍入误差时,FFT 也很自然。