引言
快速傅里叶变换(FFT)通过将多项式乘法从O(n²)加速到O(n log n),在信号处理、大整数运算等领域发挥着重要作用。然而,FFT基于浮点数运算,存在精度误差问题。数论变换(Number Theoretic Transform,NTT)是FFT在模运算域上的精确实现,利用原根替代单位复根,完全避免了浮点精度问题,在算法竞赛和密码学中有广泛应用。
算法原理
1. 从FFT到NTT
FFT的核心是利用单位复根的性质:
– ωₙ = e^(2πi/n) 是n次单位复根
– 满足 ωₙⁿ = 1 且 ωₙ^k (k=0..n-1) 互不相同
NTT将这一思想迁移到模p的整数域中:
– 寻找模p的原根g,使得 g^((p-1)/n) 是n次单位根
– 要求 n | (p-1),即n整除p-1
2. 常用模数与原根
| 模数 p | 原根 g | 最大支持长度 |
|---|---|---|
| 998244353 | 3 | 2²³ |
| 1004535809 | 3 | 2²¹ |
| 469762049 | 3 | 2²⁶ |
| 167772161 | 3 | 2²⁵ |
3. 核心性质
- 原根存在性:当且仅当 p = 2, 4, q^k, 2q^k(q为奇素数)时,模p存在原根
- 蝴蝶操作:与FFT相同的分治结构,只是运算在模p下进行
- 逆变换:利用原根的逆元实现,同样需要满足特定条件
Java实现
/**
* NTT数论变换实现
* 模数:998244353 = 119 * 2^23 + 1,原根为3
* 支持长度不超过2^23的多项式乘法
*/
public class NTT {
// 常用模数:998244353 = 119 * 2^23 + 1
static final int MOD = 998244353;
static final int G = 3; // 原根
/**
* 快速幂取模
*/
static long power(long base, long exp, int mod) {
long result = 1;
base %= mod;
while (exp > 0) {
if ((exp & 1) == 1) result = result * base % mod;
base = base * base % mod;
exp >>= 1;
}
return result;
}
/**
* 模逆元(费马小定理,要求mod为素数)
*/
static long inv(long a, int mod) {
return power(a, mod - 2, mod);
}
/**
* NTT核心变换
* @param a 多项式系数数组(会被修改)
* @param invert true表示逆变换
*/
static void ntt(long[] a, boolean invert) {
int n = a.length;
// 位逆序置换(蝴蝶操作的前提)
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) {
long temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
// 逐层进行NTT
for (int len = 2; len <= n; len <<= 1) {
// wlen = g^((mod-1)/len) 或它的逆元
long wlen = power(G, (MOD - 1) / len, MOD);
if (invert) {
wlen = inv(wlen, MOD);
}
for (int i = 0; i < n; i += len) {
long w = 1;
for (int j = 0; j < len / 2; j++) {
long u = a[i + j];
long v = a[i + j + len / 2] * w % MOD;
a[i + j] = (u + v) % MOD;
a[i + j + len / 2] = (u - v + MOD) % MOD;
w = w * wlen % MOD;
}
}
}
// 逆变换需要除以n(即乘以n的逆元)
if (invert) {
long nInv = inv(n, MOD);
for (int i = 0; i < n; i++) {
a[i] = a[i] * nInv % MOD;
}
}
}
/**
* 多项式乘法(卷积)
* @param a 多项式A的系数
* @param b 多项式B的系数
* @return 多项式A*B的系数(模998244353)
*/
static long[] multiply(long[] a, long[] b) {
if (a.length == 0 || b.length == 0) return new long[0];
// 找到不小于结果长度的最小2的幂
int resultSize = a.length + b.length - 1;
int n = 1;
while (n < resultSize) n <<= 1;
// 填充到长度n
long[] fa = new long[n];
long[] fb = new long[n];
System.arraycopy(a, 0, fa, 0, a.length);
System.arraycopy(b, 0, fb, 0, b.length);
// 正变换
ntt(fa, false);
ntt(fb, false);
// 点值相乘
for (int i = 0; i < n; i++) {
fa[i] = fa[i] * fb[i] % MOD;
}
// 逆变换
ntt(fa, true);
// 截取有效部分
long[] result = new long[resultSize];
System.arraycopy(fa, 0, result, 0, resultSize);
return result;
}
/**
* 大整数乘法(使用NTT加速)
* 将大整数按位拆分为多项式系数,相乘后再处理进位
*/
static String bigIntegerMultiply(String num1, String num2) {
if (num1.equals("0") || num2.equals("0")) return "0";
// 转换为数字数组(低位在前)
int n1 = num1.length(), n2 = num2.length();
long[] a = new long[n1];
long[] b = new long[n2];
for (int i = 0; i < n1; i++) {
a[n1 - 1 - i] = num1.charAt(i) - '0';
}
for (int i = 0; i < n2; i++) {
b[n2 - 1 - i] = num2.charAt(i) - '0';
}
// NTT卷积
long[] c = multiply(a, b);
// 处理进位
long carry = 0;
for (int i = 0; i < c.length; i++) {
long sum = c[i] + carry;
c[i] = sum % 10;
carry = sum / 10;
}
// 转换为字符串
StringBuilder sb = new StringBuilder();
int i = c.length - 1;
while (i >= 0 && c[i] == 0) i--;
while (i >= 0) {
sb.append(c[i]);
i--;
}
return sb.length() == 0 ? "0" : sb.toString();
}
/**
* 序列卷积(用于组合计数等场景)
*/
static long[] convolution(long[] a, long[] b) {
return multiply(a, b);
}
public static void main(String[] args) {
// 测试1:多项式乘法
// (1 + 2x + 3x^2) * (4 + 5x) = 4 + 13x + 22x^2 + 15x^3
long[] p1 = {1, 2, 3};
long[] p2 = {4, 5};
long[] result = multiply(p1, p2);
System.out.print("多项式乘法结果:");
for (long x : result) System.out.print(x + " ");
System.out.println(); // 应输出:4 13 22 15
// 测试2:大整数乘法
String num1 = "123456789";
String num2 = "987654321";
String product = bigIntegerMultiply(num1, num2);
System.out.println(num1 + " * " + num2 + " = " + product);
System.out.println("验证:" + (product.equals("121932631112635269") ? "正确" : "错误"));
// 测试3:大数乘法
String big1 = "12345678901234567890";
String big2 = "98765432109876543210";
String bigProduct = bigIntegerMultiply(big1, big2);
System.out.println("大数乘法结果长度:" + bigProduct.length());
}
}
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 单次NTT变换 | O(n log n) | O(n) |
| 多项式乘法 | O(n log n) | O(n) |
| 大整数乘法 | O(n log n) | O(n) |
相比朴素的O(n²)算法,NTT在处理大规模数据时优势显著。
应用场景
- 大整数运算:密码学中的RSA、ECC等需要高精度乘法
- 组合计数:生成函数相乘计算组合数
- 信号处理:离散卷积的精确计算
- 字符串匹配:基于多项式的模式匹配算法
总结
NTT是FFT在有限域上的精确实现,消除了浮点误差的同时保持了O(n log n)的高效性。理解NTT的关键在于掌握原根在单位根中的替代作用,以及模运算下的蝴蝶操作。对于需要精确结果的卷积计算,NTT是不二之选。