「快速傅里叶变换」总结
前言
作为 NOI 大纲里的十级算法,FFT拥有着逼格十分高的名称,也十分令人头疼?(雾
通俗地讲,FFT旨在以优异的时间复杂度,解决两个多项式相乘的问题,即:
c
k
=
∑
i
=
0
k
a
i
b
k
−
i
c_k=\sum_{i=0}^{k}a_ib_{k-i}
ck=i=0∑kaibk−i
FFT的核心思想在于:将一个多项式转化成点值表示法,再由点值表示法推出原多项式。
如果觉得抽象可以看图
前置知识
- 多项式的点值表示法和系数表示法
- 向量
- 复数
复数可以自行查阅 Oi-wiki。
这里简略介绍一下多项式表示法(不妨令
A
(
x
)
A(x)
A(x) 表示一个
n
−
1
n-1
n−1 次的多项式)。
系数表示法:若
A
(
x
)
=
∑
i
=
0
n
a
i
×
x
i
A(x)=\sum_{i=0}^{n}a_i\times x^{i}
A(x)=∑i=0nai×xi。
点值表示法:不妨将
n
n
n 个互不相同的
x
x
x 代入多项式得到
n
n
n 个
y
y
y,则
A
A
A 可被这
n
n
n 个值 唯一确定,其中
y
i
=
∑
j
=
0
n
−
1
a
j
×
x
i
j
y_i=\sum_{j=0}^{n-1}a_j\times x_{i}^{j}
yi=∑j=0n−1aj×xij。
单位根
请注意单位根的概念是建立在 复平面 上的,因此需要熟知复数的运算法则。
定义:以原点为圆心作单位圆,并钦定圆心为起点,圆点的 n n n 等分点为终点,作 n n n 条向量,这 n n n 条向量即为 n n n 次单位根,通常将幅角为正且最小的向量对应的复数称为 ω n \omega_n ωn。
一般地, n n n 次单位根可由 欧拉公式 直接得出: ω n k = c o s k 2 π n + i s i n k 2 π n \omega_{n}^{k}=cosk\frac{2\pi}{n}+isink\frac{2\pi}{n} ωnk=coskn2π+isinkn2π
ω n \omega_n ωn 显然满足以下性质:
- 每乘一次 ω n \omega_n ωn 便逆时针转动 2 π n \frac{2\pi}{n} n2π 角度
- ω n n = ω n \omega_{n}^{n} = \omega_{n} ωnn=ωn
-
ω
2
n
2
k
=
ω
n
k
\omega_{2n}^{2k}=\omega_{n}^{k}
ω2n2k=ωnk
快速傅里叶变换(DFT)
下皆令
n
=
2
k
(
k
∈
Z
)
n=2^k(k\in Z)
n=2k(k∈Z)
不妨令一个多项式
A
(
x
)
=
a
0
+
a
1
x
+
a
2
x
2
+
a
3
x
3
+
⋅
⋅
⋅
⋅
⋅
⋅
+
a
n
−
1
x
n
−
1
A(x)=a_0+a_1x+a_2x^{2}+a_3x^{3}+······+a_{n-1}x^{n-1}
A(x)=a0+a1x+a2x2+a3x3+⋅⋅⋅⋅⋅⋅+an−1xn−1。
将其按照下标奇偶分类,可得:
A
1
(
x
)
=
a
0
+
a
2
x
+
a
4
x
2
+
⋅
⋅
⋅
⋅
⋅
⋅
+
a
n
−
2
x
n
2
A_1(x)=a_0+a_2x+a_4x^{2}+······+a_{n-2}x^{\frac{n}{2}}
A1(x)=a0+a2x+a4x2+⋅⋅⋅⋅⋅⋅+an−2x2n
A
2
(
x
)
=
a
1
+
a
3
x
+
a
5
x
2
+
⋅
⋅
⋅
⋅
⋅
⋅
+
a
n
−
1
x
n
2
A_2(x)=a_1+a_3x+a_5x^{2}+······+a_{n-1}x^{\frac{n}{2}}
A2(x)=a1+a3x+a5x2+⋅⋅⋅⋅⋅⋅+an−1x2n
显然有,
A
(
x
)
=
A
1
(
x
2
)
+
x
A
2
(
x
2
)
A(x)=A_1(x^2)+xA_{2}(x^2)
A(x)=A1(x2)+xA2(x2)。
注意到,多项式
A
(
x
)
A(x)
A(x) 可被
n
n
n 个值唯一确定,所以,如果我们可以利用上述式子,代入特殊的
x
′
x\prime
x′ 值,以高效的时间复杂度得到
A
(
x
′
)
A(x\prime)
A(x′) 对应的值,那么便可以用 点值表示法 表示
A
(
x
)
A(x)
A(x) 了。
代入
ω
n
k
\omega_{n}^{k}
ωnk,则有:
A
(
ω
n
k
)
=
A
1
(
ω
n
2
k
)
+
ω
n
k
A
2
(
ω
n
2
k
)
A(\omega_{n}^{k})=A_1(\omega_{n}^{2k})+\omega_{n}^{k}A_2(\omega_{n}^{2k})
A(ωnk)=A1(ωn2k)+ωnkA2(ωn2k)
A
(
ω
n
k
)
=
A
1
(
ω
n
2
k
)
+
ω
n
k
A
2
(
ω
n
2
k
)
A(\omega_{n}^{k})=A_1(\omega_{\frac{n}{2}}^{k})+\omega_{n}^{k}A_2(\omega_{\frac{n}{2}}^{k})
A(ωnk)=A1(ω2nk)+ωnkA2(ω2nk)
代入
ω
n
k
+
n
2
\omega_{n}^{k+\frac{n}{2}}
ωnk+2n,同理可得:
A
(
ω
n
k
+
n
2
)
=
A
1
(
ω
n
2
k
)
−
ω
n
k
A
2
(
ω
n
2
k
)
A(\omega_{n}^{k+\frac{n}{2}})=A_1(\omega_{\frac{n}{2}}^{k})-\omega_{n}^{k}A_2(\omega_{\frac{n}{2}}^{k})
A(ωnk+2n)=A1(ω2nk)−ωnkA2(ω2nk)
通过大眼观察法可以得出,当我们计算出
A
(
ω
n
k
)
A(\omega_{n}^{k})
A(ωnk) 时,我们同时也得到了
A
(
ω
n
k
+
n
2
)
A(\omega_{n}^{k+\frac{n}{2}})
A(ωnk+2n) 的值,这便是又名的 蝴蝶操作。
于是,我们把原问题分割成了两个等价的子问题。这也就意味着,通过不断递归,我们将
n
n
n 个单位根
ω
n
k
\omega_{n}^{k}
ωnk 依次代入,便可以实现以
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn) 的时间复杂度用 点值表示法 表示多项式
A
(
x
)
A(x)
A(x)!
快速傅里叶逆变换(IDFT)
然而,题目通常要求我们用 系数表示法 表示一个多项式,而非 点值表示法。因此,我们需要将 点值 转化为 系数。
结论:IDFT 求解的矩阵即为 DFT 的逆矩阵,则我们可得:
形式化地讲,令
B
(
x
)
=
I
D
F
T
(
A
(
x
)
)
B(x)=IDFT(A(x))
B(x)=IDFT(A(x)),则有:
b
k
=
1
n
∑
i
=
0
a
i
ω
n
−
i
k
=
1
n
A
(
ω
n
−
k
)
b_k=\frac{1}{n}\sum_{i=0}a_i\omega_n^{-ik}=\frac{1}{n}A(\omega_n^{-k})
bk=n1i=0∑aiωn−ik=n1A(ωn−k)
由此,我们只需要将 DFT 实现过程中的 ω n \omega_n ωn 修改为 ω n − 1 \omega_n^{-1} ωn−1 ,最后将所得结果乘上 1 n \frac{1}{n} n1 即可。
真正的快速傅里叶变换(FFT)
结合 DFT 与 IDFT,我们实现了
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn) 的多项式乘法。
然而,多次递归导致程序的实际效率并不高,甚至过不了模板题。所以,我们考虑迭代实现 FFT。
正难则反,如果我们可以正向分割问题处理,为何不可以倒着合并得出答案呢?
同样地,如果觉得抽象可以看图。
不妨令
a
a
a 表示初始数组,
b
b
b 表示分治完后的数组,
r
e
v
i
rev_i
revi 表示
i
i
i 二进制位反转后所得到的数。
通过大眼观察法,可以得到:
a
i
=
b
r
e
v
i
a_i=b_{rev_i}
ai=brevi,因此,我们可以通过
b
b
b 数组逆推得到答案。
NTT
某些题目要求我们在模意义下输出答案,这个时候,FFT便可能会产生较大的精度误差。为了解决这类模数问题,NTT 应运而生。
FFT 无法在计算中途取模,归咎于我们代入了复数意义下的单位根。因此,我们可以规避掉单位根,转而代入模意义下与单位根有着相似性质的数,这便是 NTT。
我们通常将这个数称为 原根(
g
g
g)。
注意到 FFT 处理的数组的长度一定是
2
2
2 的整次幂,因此,我们的模数必须满足
p
=
k
2
x
+
1
p=k2^x+1
p=k2x+1 的苛刻条件。
常见地,
998244353
,
469762049
,
1004535809
998244353,469762049,1004535809
998244353,469762049,1004535809 的原根是
3
3
3。
于是,我们只需要将 FFT 中的 ω n \omega_n ωn 替换为原根即可。具体地,用 g n 1 g_n^1 gn1 满足 [ g n 1 ≡ g p − 1 n ( m o d p ) ] [g_n^1 \equiv g^{\frac{p-1}{n}} \pmod{p} ] [gn1≡gnp−1(modp)] 替换 ω n \omega_n ωn,作逆运算的时候用逆元即可。
代码
给出我常用的 FFT,NTT模板。
#include<bits/stdc++.h>
#define db double
// #define int long long
using namespace std;
const int MAXN = 1e6 + 5;
const db Pi = acos(-1.0);
namespace poly{
const int MOD = 998244353;
int to[MAXN] , G0 = 3 , G1 = 332748118 , wn[MAXN][2];
int qpow(int base , int k) {
int res = 1;
while(k) {
if (k & 1) res = res * base % MOD;
base = base * base % MOD;
k >>= 1;
}
return res;
}
int inv(int p) {return qpow(p , MOD - 2);}
struct Complex{
db x , y;
Complex() {}
Complex(db _x , db _y) {x = _x , y = _y;}
friend Complex operator + (Complex a , Complex b) {return Complex(a.x + b.x , a.y + b.y);};
friend Complex operator - (Complex a , Complex b) {return Complex(a.x - b.x , a.y - b.y);};
friend Complex operator * (Complex a , Complex b) {return Complex(a.x * b.x - a.y * b.y , a.x * b.y + a.y * b.x);};
friend Complex operator / (Complex a , db b) {return Complex(a.x / b , a.y / b);};
friend Complex operator * (Complex a , db b) {return Complex(a.x * b , a.y * b);};
}a[MAXN] , b[MAXN] , q1[MAXN] , q2[MAXN] , q3[MAXN];
void init(int len) {
int lg = __lg(len) + 1 ,limit = (1 << lg);
for (int i = 0 ; i < limit ; i ++) to[i] = (to[i >> 1] >> 1) | ((i & 1) << (lg - 1));
wn[lg][1] = qpow(G1 , (MOD - 1) / limit);
wn[lg][0] = qpow(G0 , (MOD - 1) / limit);
for (int i = lg ; i >= 1 ; i --) {
wn[i - 1][0] = wn[i][0] * wn[i][0] % MOD;
wn[i - 1][1] = wn[i][1] * wn[i][1] % MOD;
}
}
void FFT(Complex *A , int up , int type){
for (int i = 0 ; i < up ; i ++) {
if (i > to[i]) swap(A[i] , A[to[i]]);
}
for (int mid = 1 ; mid < up ; mid <<= 1) {
Complex Wn(cos(Pi / mid) , type * sin(Pi / mid));
for (int len = mid << 1 , j = 0 ; j < up ; j += len) {
Complex W(1 , 0);
for (int i = 0 ; i < mid ; i ++ , W = W * Wn) {
Complex tmpx = A[j + i] , tmpy = W * A[j + mid + i];
A[j + i] = tmpx + tmpy , A[j + mid + i] = tmpx - tmpy;
}
}
}
}
void NTT(int *A , int up , int type) {
for (int i = 0 ; i < up ; i ++) {
if (i > to[i]) swap(A[i] , A[to[i]]);
}
for (int mid = 1 , lg = 1 ; mid < up ; mid <<= 1 , lg ++) {
int Wn = wn[lg][type];
for (int len = mid << 1 , j = 0 ; j < up ; j += len) {
int W = 1;
for (int i = 0 ; i < mid ; i ++ , W = W * Wn % MOD) {
int tmpx = A[j + i] , tmpy = W * A[j + mid + i] % MOD;
A[j + i] = (tmpx + tmpy) % MOD , A[j + mid + i] = (tmpx - tmpy + MOD) % MOD;
}
}
}
if (!type) return;
int inv = qpow(up , MOD - 2);
for (int i = 0 ; i < up ; i ++) A[i] = A[i] * inv % MOD;
}
void FFT_mul(int n , db *F , int m , db *G , db *res) {
init(n + m);
int up = __lg(n + m) + 1;
up = (1 << up);
for (int i = 0 ; i <= up ; i ++) a[i].x = a[i].y = b[i].x = b[i].y = 0;
for (int i = 0 ; i <= n ; i ++) a[i].x = F[i];
for (int i = 0 ; i <= m ; i ++) b[i].x = G[i];
FFT(a , up , 1) , FFT(b , up , 1);
for (int i = 0 ; i <= up ; i ++) a[i] = a[i] * b[i];
FFT(a , up , -1);
for (int i = 0 ; i <= n + m ; i ++) res[i] = a[i].x;
}
void NTT_mul(int n , int *F , int m , int *G , int *res) {
init(n + m);
int up = __lg(n + m) + 1;
up = (1 << up);
NTT(F , up , 0) , NTT(G , up , 0);
for (int i = 0 ; i < up ; i ++) F[i] = F[i] * G[i] % MOD;
NTT(F , up , 1);
for (int i = 0 ; i < up ; i ++) res[i] = F[i];
}
}
int n , m;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr) , cout.tie(nullptr);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】