FFT快速傅里叶变换与NTT快速数论变换

本文介绍了FFT快速傅里叶变换和NTT快速数论变换在大数乘法和公钥密码学中的应用。通过离散傅里叶变换(DFT)和离散傅里叶逆变换(IDFT)阐述FFT的基本步骤,并探讨NTT中原根的概念及其与单位复根的相似性,强调在格上后量子密钥交换和基于RLWE问题的密码算法中NTT的重要性。

前言

大数相乘是公钥密码学中一个非常关键的运算,公钥密码学中往往需要进行大量的大整数相乘运算,它的性能影响着许多公钥密码算法的性能[1]。

最近在阅读格上后量子密钥交换相关论文的过程中,我发现在基于RLWE(Ring learning with errors, 环上容错学习)问题的格密钥交换方案中会进行多项式相乘运算,而其中往往会使用到NTT快速数论变换算法,快速数论变换(NTT)是快速傅里叶变换(FFT)在数论基础上的实现,为了弄懂其中的原理,我对FFT快速傅里叶变换以及NTT快速数论变换的相关知识进行了学习。


基础知识

在学习FFT以及NTT之前,我们应该先对多项式有一个基本的了解。
多项式有两种基本的表示方法,一种是系数表示,一种是点值表示;对于一个已知的多项式 A ( x ) = a 0 + a 1 x + a 2 x 2 + ⋯ + a n − 1 x n − 1 A(x)=a_0+a_1 x+a_2 x^2+⋯+a_{n-1} x^{n-1} A(x)=a0+a1x+a2x2++an1xn1,

  • 系数表示法:用 n n n个系数确定一个 n − 1 n-1 n1次多项式, ( a 0 , a 1 , a 2 , … , a n − 1 ) (a_0,a_1,a_2,…,a_{n-1}) (a0,a1,a2,,an1)
  • 点值表示法:对于多项式 A ( x ) A(x) A(x),将 x 0 x_0 x0代进去可以得到一个确定的值 y 0 y_0 y0,并且可以将 ( x 0 , y 0 ) (x_0,y_0) (x0,y0)看作是坐标系上的一个点;用 n n n个点可以确定一个 n − 1 n-1 n1次多项式, ( x 0 , x 1 , x 2 , … , x n − 1 ) (x_0,x_1,x_2,…,x_{n-1} ) (x0,x1,x2,,xn1) ( y 0 , y 1 , y 2 , … , y n − 1 ) (y_0,y_1,y_2,…,y_{n-1}) (y0,y1,y2,,yn1)

现在有两个多项式 A ( x ) , B ( x ) A(x),B(x) A(x),B(x),两个多项式相乘结果为 C ( x ) C(x) C(x),这个就叫多项式的乘法,多项式乘法的结果又可以叫做这两个多项式的卷积。某种程度上,FFT就是在解决多项式乘法的问题。


FTT

快速傅里叶变换(英语:Fast Fourier Transform, FFT),是快速计算串行的离散傅里叶变换(DFT)或其逆变换的方法[2]。直接暴力求解两个多项式的卷积,复杂度是 O ( n 2 ) O(n^2) O(n2)的,而FFT可以将这一过程的复杂度降到 O ( n l o g ⁡ n ) O(n log⁡n) O(nlogn),具体过程分为两个过程:离散傅里叶变换(DFT)和离散傅里叶逆变换(IDFT)。通俗地来说,多项式由系数表示法转为点值表示法的过程,就是DFT,多项式由点值表示法转化为系数表示法的过程,就是IDFT;而FFT就是通过取某些特殊的x的点值来加速DFT和IDFT的过程[3]。
FFT通过找到一组特殊的点代入到多项式中,从而快速计算出一组 A ( x ) , B ( x ) A(x),B(x) A(x),B(x)的点值,而这组特殊点就是复数中的单位根。在复平面上,一个复数可以用一个向量表示,所有在单位圆上的向量都可以称为一个单位根。考虑将这个单位圆从 x x x轴开始等分成 n n n份,其中得到的这 n n n个单位根,就是 n n n次单位根,其中的第 k k k个可以记为 ω n k ω_n^k ωnk,且 ω n 0 = 1 ω_n^0=1 ωn0=1


图1 复平面上的3次单位根

众所周知, e i θ = c o s ⁡ θ + i ⋅ s i n ⁡ θ e^iθ=cos⁡θ+i·sin⁡θ eiθ=cosθ+isinθ,所以
ω n k = c o s ⁡ ( 2 π ⋅ k n ) + i ⋅ s i n ⁡ ( 2 π ⋅ k n ) = e 2 π ⋅ k n i ω_n^k=cos⁡(2π·\frac{k}{n})+i·sin⁡(2π·\frac{k}{n})=e^{2π·\frac{k}{n} i} ωnk=cos(2πnk)+isin(2πnk)=e2πnki
显然,单位根具有以下一些性质:
性质一: ω n k = ω 2 n 2 k ω_n^k=ω_{2n}^{2k} ωnk=ω2n2k
性质二: ω n k + n 2 = − ω n k ω_n^{k+\frac{n}{2}}=-ω_n^k ωnk+2n=ωnk
性质三: ω n k ⋅ ω n l = ω n k + l ω_n^k·ω_n^l=ω_n^{k+l} ωnkωnl=ωnk+l
接下来我们来看FFT的具体过程是如何实现的。

第一步:离散傅里叶变换(DFT)

考虑一个 n = 2 b n=2^b n=2b项的多项式 A ( x ) A(x) A(x)系数表示为 ( a 0 , a 1 , a 2 , … , a n − 1 ) (a_0,a_1,a_2,…,a_{n-1}) (a0,a1,a2,,an1),现在我们代入一组单位根 ( ω n 0 , ω n 1 , ω n 2 , … , ω n n − 1 ) (ω_n^0,ω_n^1,ω_n^2,…,ω_n^{n-1})

<think>快速数论变换NTT)是离散傅里叶变换(DFT)在有限域(模运算)上的实现,常用于高效计算多项式乘法(卷积)。FFT相比,NTT在整数环上运算,避免了浮点数精度问题,特别适用于模数意义下的多项式运算(如密码学、组合数学)。 NTT要求模数必须满足存在原根(即模数是形如$p = c \cdot 2^k + 1$的质数),常见模数如998244353(原根为3)、1004535809(原根为3)等。 ### NTT原理 设质数$p$,其原根为$g$,则模$p$意义下的$n$次单位根为$g^{(p-1)/n}$,其中$n$是2的幂次且$n \mid (p-1)$。 NTT变换: $$ X_k = \sum_{j=0}^{n-1} x_j \cdot (g^{(p-1)/n})^{kj} \mod p $$ 逆变换(INTT): $$ x_j = n^{-1} \sum_{k=0}^{n-1} X_k \cdot (g^{-(p-1)/n})^{kj} \mod p $$ ### C++实现NTT 以下代码实现了NTT及其逆变换,并包含多项式乘法示例。 ```cpp #include <iostream> #include <vector> #include <algorithm> using namespace std; const long long MOD = 998244353; // 常用NTT模数 const long long ROOT = 3; // 模数的原根 const long long ROOT_INV = 332748118; // 原根的逆元 // 快速幂取模 long long mod_pow(long long base, long long exp, long long mod = MOD) { long long result = 1; while (exp) { if (exp & 1) result = (result * base) % mod; base = (base * base) % mod; exp >>= 1; } return result; } // 求逆元 inline long long mod_inv(long long a, long long mod = MOD) { return mod_pow(a, mod - 2, mod); } // 位逆序置换 void bit_reverse(vector<long long>& a) { 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]); } } // NTT核心函数(invert=false为正变换,invert=true为逆变换) void ntt(vector<long long>& a, bool invert) { int n = a.size(); bit_reverse(a); for (int len = 2; len <= n; len <<= 1) { long long wlen = invert ? ROOT_INV : ROOT; // 计算单位根:wlen = g^{(p-1)/len} mod p for (int i = len; i < (MOD-1)>>1; i <<= 1) wlen = (wlen * wlen) % MOD; for (int i = 0; i < n; i += len) { long long w = 1; for (int j = 0; j < len / 2; j++) { long long u = a[i+j]; long 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; } } } if (invert) { long long inv_n = mod_inv(n, MOD); for (long long& x : a) x = (x * inv_n) % MOD; } } // 多项式乘法(卷积) vector<long long> poly_multiply(vector<long long> a, vector<long long> b) { int n = 1; while (n < a.size() + b.size()) n <<= 1; a.resize(n); b.resize(n); ntt(a, false); ntt(b, false); for (int i = 0; i < n; i++) a[i] = (a[i] * b[i]) % MOD; ntt(a, true); return a; } int main() { // 示例:计算 (x+2)(3x+4) = 3x^2 + 10x + 8 vector<long long> A = {2, 1}; // 多项式A: x+2 vector<long long> B = {4, 3}; // 多项式B: 3x+4 vector<long long> C = poly_multiply(A, B); cout << "Coefficients of product polynomial: "; for (long long x : C) { cout << x << " "; } // 输出: 8 10 3 (对应常数项、一次项、二次项系数) // 因为:8 + 10x + 3x^2 return 0; } ``` ### 关键步骤解析 1. **位逆序置换**: - 通过位操作将数组元素按分治所需顺序重新排列,避免递归。 - 时间复杂度$O(n)$。 2. **蝶形运算**: - 在每一层循环中,将数组分成大小为`len`的块,对每块进行蝶形运算。 - 使用单位根`wlen`的幂次进行线性变换。 3. **逆变换处理**: - 逆变换时,使用原根的逆元`ROOT_INV`作为单位根。 - 最后乘以$n^{-1}$($n$的模逆元)实现归一化。 4. **多项式乘法**: - 通过三次NTT(两次正变换、一次逆变换)实现卷积。 - 复杂度$O(n \log n)$,远优于朴素$O(n^2)$。 ### 注意事项 - 数组长度必须为2的幂次,不足时补零。 - 模数必须满足$p = c \cdot 2^k + 1$,且$2^k$大于等于多项式长度。 - 原根原根逆元需要预先计算。 ### 性能优化 1. 预处理单位根:避免重复计算单位根的幂次。 2. 使用迭代版避免递归开销。 3. 使用constexprinline加速模运算。 §§相关问题§§: 1. NTTFFT在性能适用场景上有何区别? 2. 如何实现任意模数下的多项式乘法(如非NTT友好模数)? 3. 请解释如何利用NTT计算大整数乘法? 4. 在密码学中,NTT有哪些具体应用场景?</think># 快速数论变换NTT)详解 快速数论变换NTT)是离散傅里叶变换(DFT)在有限域上的实现,特别适用于模数意义下的多项式乘法。FFT相比,NTT使用整数运算而非浮点数,避免了精度问题,特别适合密码学、多项式运算等需要精确整数的场景。 ## NTT数学原理 给定质数模数 $p$ 原根 $g$,NTT定义在模 $p$ 的整数环上。对于长度为 $n$ 的多项式($n$ 是2的幂),NTT变换为: $$X_k = \sum_{j=0}^{n-1} x_j \cdot g^{j k \frac{p-1}{n}} \mod p$$ 逆变换(INTT)为: $$x_j = n^{-1} \sum_{k=0}^{n-1} X_k \cdot g^{-j k \frac{p-1}{n}} \mod p$$ 其中 $g$ 是模 $p$ 的原根,满足 $g^{(p-1)/n}$ 是 $n$ 次单位根。 ## C++实现NTT 以下是完整的NTT实现,包含多项式乘法示例: ```cpp #include <iostream> #include <vector> #include <algorithm> using namespace std; // 常用NTT模数(998244353 = 119×2^23 + 1) const long long MOD = 998244353; const long long ROOT = 3; // 模数的原根 const long long ROOT_INV = 332748118; // 原根的逆元 (3^{-1} mod MOD) // 快速幂取模 long long mod_pow(long long base, long long exp, long long mod = MOD) { long long result = 1; while (exp) { if (exp & 1) result = (result * base) % mod; base = (base * base) % mod; exp >>= 1; } return result; } // 求逆元 inline long long mod_inv(long long a, long long mod = MOD) { return mod_pow(a, mod - 2, mod); } // 位逆序置换 void bit_reverse(vector<long long>& a) { 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]); } } // NTT核心函数(invert=false为正变换,invert=true为逆变换) void ntt(vector<long long>& a, bool invert) { int n = a.size(); bit_reverse(a); for (int len = 2; len <= n; len <<= 1) { // 计算当前长度的单位根 long long wlen = invert ? ROOT_INV : ROOT; for (int i = len; i < (MOD-1)>>1; i <<= 1) wlen = (wlen * wlen) % MOD; // 蝶形变换 for (int i = 0; i < n; i += len) { long long w = 1; for (int j = 0; j < len / 2; j++) { long long u = a[i+j]; long 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^{-1} if (invert) { long long inv_n = mod_inv(n, MOD); for (long long& x : a) x = (x * inv_n) % MOD; } } // 多项式乘法(卷积) vector<long long> poly_multiply(vector<long long> a, vector<long long> b) { // 扩展为2的幂次 int n = 1; while (n < a.size() + b.size()) n <<= 1; a.resize(n); b.resize(n); // NTT变换 ntt(a, false); ntt(b, false); // 点值乘法 for (int i = 0; i < n; i++) a[i] = (a[i] * b[i]) % MOD; // 逆变换 ntt(a, true); return a; } int main() { // 示例1:计算 (x+2)(3x+4) = 3x^2 + 10x + 8 vector<long long> A = {2, 1}; // 多项式A: x+2 vector<long long> B = {4, 3}; // 多项式B: 3x+4 vector<long long> C = poly_multiply(A, B); cout << "Coefficients of (x+2)(3x+4): "; for (long long x : C) { cout << x << " "; } // 输出: 8 10 3 (对应常数项、一次项、二次项系数) // 示例2:计算 (x^2 + 2x + 3)(4x + 5) vector<long long> D = {3, 2, 1}; // x^2 + 2x + 3 vector<long long> E = {5, 4}; // 4x + 5 vector<long long> F = poly_multiply(D, E); cout << "\nCoefficients of (x^2+2x+3)(4x+5): "; for (long long x : F) { cout << x << " "; } // 输出: 15 22 14 4 (对应常数项、一次项、二次项、三次项系数) // 即 4x^3 + 14x^2 + 22x + 15 return 0; } ``` ## 关键组件解析 ### 1. 位逆序置换 (bit_reverse) ```cpp 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]); } ``` - 将数组元素按分治需要的顺序重新排列 - 时间复杂度 $O(n)$,是迭代实现NTT的关键 ### 2. 蝶形变换 (Butterfly Operation) ```cpp for (int i = 0; i < n; i += len) { long long w = 1; for (int j = 0; j < len / 2; j++) { long long u = a[i+j]; long 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; } } ``` - 核心计算单元,处理长度为`len`的分组 - 使用单位根的幂次`w`进行线性组合 - 每次迭代处理长度加倍的分组 ### 3. 逆变换处理 ```cpp if (invert) { long long inv_n = mod_inv(n, MOD); for (long long& x : a) x = (x * inv_n) % MOD; } ``` - 逆变换后需要乘以$n^{-1}$进行归一化 - 使用费马小定理计算模逆元 ## NTT模数选择 常用NTT友好质数: 1. $998244353 = 119×2^{23} + 1$(原根=3) 2. $1004535809 = 479×2^{21} + 1$(原根=3) 3. $469762049 = 7×2^{26} + 1$(原根=3) 这些质数满足$p = k×2^m + 1$的形式,支持足够大的变换长度。 ## 性能优化技巧 1. **预处理单位根**:避免重复计算$g^{(p-1)/len}$ 2. **使用constexpr计算**:编译时计算常量表达式 3. **循环展开**:手动展开内层循环 4. **SIMD指令**:使用AVX2等指令集并行化计算 5. **缓存友好访问**:优化内存访问模式 ## 应用场景 1. 多项式乘法(卷积) 2. 大整数乘法(将整数视为多项式) 3. 密码学(格密码、同态加密) 4. 组合计数(生成函数计算) 5. 信号处理(有限域上的滤波) ## 时间复杂度 | 操作 | 时间复杂度 | |------|------------| | 单次NTT/INTT | $O(n \log n)$ | | 多项式乘法 | $O(n \log n)$ | | 大整数乘法 | $O(n \log n)$ | 相比朴素$O(n^2)$算法,NTT在$n>100$时具有显著优势。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值