算法学习FFT系列(1):初习快速傅里叶变换
引入
这个坑已经在我脑海里占了很久了,但是一直没有水平写,今天尝试着写写看FFT的算法学习。
FFT在OI中最大的作用是加速卷积。理论上背板子是没毛病的,但是仍然遇到了一些考定义的毒瘤题,所以还是理解比较好。
多项式乘法
定义
多项式
A(x)=∑inai∗xi,B(x)=∑inbi∗xi
A
(
x
)
=
∑
i
n
a
i
∗
x
i
,
B
(
x
)
=
∑
i
n
b
i
∗
x
i
多项式乘法就是
C(x)=∑i2n−1(∑jiaj∗bi−j)xi C ( x ) = ∑ i 2 n − 1 ( ∑ j i a j ∗ b i − j ) x i
显然,多项式乘法需要 O(n2) O ( n 2 ) 的复杂度。
表示方法
普通的多项式表示方法把n阶多项式
A(x)
A
(
x
)
表示为向量
A
A
这里需要引入一种全新的表示方法——点值表示法。
也就是表示为
A(A(x0),A(x1),A(x2)⋯A(xn))
A
(
A
(
x
0
)
,
A
(
x
1
)
,
A
(
x
2
)
⋯
A
(
x
n
)
)
n阶多项式和n个互不相同的点值表示一一对应。
这样的表示方法的优点是什么?考虑多项式乘法的过程。
C(x)=∑i2n−1(∑jiaj∗bi−j)xi C ( x ) = ∑ i 2 n − 1 ( ∑ j i a j ∗ b i − j ) x i
=∑i2n−1∑ji(aj∗xj)∗(bi−j∗xi−j) = ∑ i 2 n − 1 ∑ j i ( a j ∗ x j ) ∗ ( b i − j ∗ x i − j )
=∑j2n−1(aj∗xj)∗∑i2n−1(bi∗xi) = ∑ j 2 n − 1 ( a j ∗ x j ) ∗ ∑ i 2 n − 1 ( b i ∗ x i )
转化成点值表示的两个式子,不难发现,其乘法复杂度是 O(n) O ( n ) 的
FFT的总路线:系数表达式->点值表达式->乘法->点值表达式->系数表达式
插值(*)
这个东西是顺便一提,FFT中系数表达式<->点值表达式过程并不是FFT的专属,但是确是FFT的关键。这一过程被称之为插值。证明插值的唯一性(也就是n阶多项式和n个互不相同的点值表示一一对应。)需要通过范德蒙德矩阵的可逆性。
⎡⎣⎢⎢⎢⎢11…1x0x1…xnx20x21…x2n…⋯…⋯xn0xn1…xnn⎤⎦⎥⎥⎥⎥⎡⎣⎢⎢⎢a0a1…an⎤⎦⎥⎥⎥=⎡⎣⎢⎢⎢y0y1…yn⎤⎦⎥⎥⎥
[
1
x
0
x
0
2
…
x
0
n
1
x
1
x
1
2
⋯
x
1
n
…
…
…
…
…
1
x
n
x
n
2
⋯
x
n
n
]
[
a
0
a
1
…
a
n
]
=
[
y
0
y
1
…
y
n
]
左边的矩阵表示为
V(x0,x1…xn)
V
(
x
0
,
x
1
…
x
n
)
就是范德蒙德矩阵
证明不会,自行百度。
插值相关还有这些东西,可以看看算法学习:拉格朗日插值
既然是点值表达式,最重要的就是带入什么点值,这里我们引入一个新的概念——单位复根。
单位复根
n次单位复根
n
次
单
位
复
根
的表达式是
ωn=1
ω
n
=
1
,更形象地,我们可以通过复数运算的几何意义(幅角相加, 模相乘)得到下面两张图
我们可以得到n次单位复根有n个,均匀分布在复平面上半径为1的圆上。
欧拉公式与单位复根表示
eix=cosx+isinx
e
i
x
=
c
o
s
x
+
i
s
i
n
x
证明的方法是泰勒展开。
带入
x=2πk
x
=
2
π
k
得到
e2πki=cos(2πk)+isin(2πk)=1=ωn
e
2
π
k
i
=
c
o
s
(
2
π
k
)
+
i
s
i
n
(
2
π
k
)
=
1
=
ω
n
于是
ωkn=e2πkin,k=0,1…n
ω
n
k
=
e
2
π
k
i
n
,
k
=
0
,
1
…
n
其实
ωkn
ω
n
k
构成了一个乘法群。。。不加以赘述。
有了表达式,我们就可以挖掘
ωkn
ω
n
k
的性质了。
各种定理
消去引理
ωdkdn=ωkn ω d n d k = ω n k
证明: ωdkdn=e2πdkidn=e2πkin=ωkn ω d n d k = e 2 π d k i d n = e 2 π k i n = ω n k
折半引理
如果 n>0 n > 0 且n为偶数,那么n个n次单位复根的平方的集合就是 n2 n 2 个 n2 n 2 次单位复根的集合
证明:其实就是证明两个东西(1) (ωkn)2=ωkn2 ( ω n k ) 2 = ω n 2 k (2) ) (ωk+n2n)2=(ωkn)2 ( ω n k + n 2 ) 2 = ( ω n k ) 2
(1)(ωkn)2=ω2kn=ωkn2 ( 1 ) ( ω n k ) 2 = ω n 2 k = ω n 2 k
(2)(ωk+n2n)2=ω2k+nn=ωknωnn=(ωkn)2 ( 2 ) ( ω n k + n 2 ) 2 = ω n 2 k + n = ω n k ω n n = ( ω n k ) 2
求和引理
∑jn−1(ωkn)j=0 ∑ j n − 1 ( ω n k ) j = 0
证明: ∑j=0n−1(ωkn)j=(wkn)n−1wkn−1=(wnn)k−1wkn−1=0(kmodn≠0) ∑ j = 0 n − 1 ( ω n k ) j = ( w n k ) n − 1 w n k − 1 = ( w n n ) k − 1 w n k − 1 = 0 ( k mod n ≠ 0 )
特别地: ∑j=0n−1(ωkn)j=n(kmodn=0) ∑ j = 0 n − 1 ( ω n k ) j = n ( k mod n = 0 )
前置技能已经get得差不多了,开始表演
离散型傅里叶变换和逆离散型傅里叶变换(DFT和IDFT)
应该还记得快速傅里叶变换要干什么吧。
快速插值。
我们要做的事情就是
(1) 已知A(x)=(a0,a1…an),求A(x)=(A(ω0),A(ω1)…A(ωn)) 已 知 A ( x ) = ( a 0 , a 1 … a n ) , 求 A ( x ) = ( A ( ω 0 ) , A ( ω 1 ) … A ( ω n ) )
(2) 已知A(x)=(A(ω0),A(ω1)…A(ωn)),求A(x)=(a0,a1…an) 已 知 A ( x ) = ( A ( ω 0 ) , A ( ω 1 ) … A ( ω n ) ) , 求 A ( x ) = ( a 0 , a 1 … a n )
刚才介绍了这么多优秀的单位复根的性质,于是我们容易想到,把单位复根带入多项式里面。(为了方便,我们用
ω
ω
表示
ωn
ω
n
)
假设序列
A和B
A
和
B
乘法之后得到的序列是
C
C
,其长度是长度之和减一。我们找到一个最小n使得n大于C的长度并且n是2的整数次幂(为啥?之后会提到。)
我们可以把卷积变成这样的形式。
cr=∑p,q[p+qmodn=r]apbq
c
r
=
∑
p
,
q
[
p
+
q
mod
n
=
r
]
a
p
b
q
由求和引理可得
1n∑j=0n−1(ωk)j=[kmodn=0]
1
n
∑
j
=
0
n
−
1
(
ω
k
)
j
=
[
k
mod
n
=
0
]
于是
[p+qmodn=r]=[p+q−rmodn=0]=1n∑k=0n−1(ωp+q−r)k=1n∑k=0n−1ω−rkωpkωqk
[
p
+
q
mod
n
=
r
]
=
[
p
+
q
−
r
mod
n
=
0
]
=
1
n
∑
k
=
0
n
−
1
(
ω
p
+
q
−
r
)
k
=
1
n
∑
k
=
0
n
−
1
ω
−
r
k
ω
p
k
ω
q
k
cr=∑p,q[p+q−rmodn=0]apbq
c
r
=
∑
p
,
q
[
p
+
q
−
r
mod
n
=
0
]
a
p
b
q
=∑p,q1n∑k=0n−1ω−rkωpkωqkapbq
=
∑
p
,
q
1
n
∑
k
=
0
n
−
1
ω
−
r
k
ω
p
k
ω
q
k
a
p
b
q
=1n∑k=0n−1ω−rk∑pωpkap∑qωqkbq
=
1
n
∑
k
=
0
n
−
1
ω
−
r
k
∑
p
ω
p
k
a
p
∑
q
ω
q
k
b
q
=1n∑k=0n−1ω−rkA(ωk)B(ωk)=1n∑k=0n−1ω−rkC(ωk)
=
1
n
∑
k
=
0
n
−
1
ω
−
r
k
A
(
ω
k
)
B
(
ω
k
)
=
1
n
∑
k
=
0
n
−
1
ω
−
r
k
C
(
ω
k
)
刚才经过一波推导我们成功地找到了
C
C
点值表达式和系数表达式的关系。
这样子的话问题转化为
(1) 求A(ωk)=∑pωkpap 求 A ( ω k ) = ∑ p ω k p a p
(2) 求ar=1n∑rω−krA(ωk) 求 a r = 1 n ∑ r ω − k r A ( ω k )
前者即为DFT,后者即是IDFT
我们不难发现,两者的过程惊人的相似,其实只是多了
1n
1
n
和一个-
于是我们使用同一个算法——快速傅里叶算法(FFT)来实现这玩意儿。
快速傅里叶变换(FFT)
设
A0(x)是A(x)偶次项的和,A1(x)是A(x)奇次项的和
A
0
(
x
)
是
A
(
x
)
偶
次
项
的
和
,
A
1
(
x
)
是
A
(
x
)
奇
次
项
的
和
注意到
(ωmn)2=(ωm+n2n)2=ωmn2
(
ω
n
m
)
2
=
(
ω
n
m
+
n
2
)
2
=
ω
n
2
m
A(ωmn)=A0((ωmn)2)+ωmnA1((ωmn)2)=A0(ωmn2)+ωmnA1(ωmn2)
A
(
ω
n
m
)
=
A
0
(
(
ω
n
m
)
2
)
+
ω
n
m
A
1
(
(
ω
n
m
)
2
)
=
A
0
(
ω
n
2
m
)
+
ω
n
m
A
1
(
ω
n
2
m
)
A(ωm+n2n)=A0((ωmn)2)+ωm+n2nA1((ωmn)2)=A0(ωmn2)−ωmnA1(ωmn2)
A
(
ω
n
m
+
n
2
)
=
A
0
(
(
ω
n
m
)
2
)
+
ω
n
m
+
n
2
A
1
(
(
ω
n
m
)
2
)
=
A
0
(
ω
n
2
m
)
−
ω
n
m
A
1
(
ω
n
2
m
)
这就是单位复根最英霸的地方,折半引理和消去引理可以使得它能够把插值的过程分治。上述操作被称为蝴蝶操作。
上文有提到,n是2的整数次幂,所以这个过程可以一直递推下去。不难发现,上述算法的时间复杂度递推式是。
T(n)=2T(n2)+O(logn)
T
(
n
)
=
2
T
(
n
2
)
+
O
(
l
o
g
n
)
由主定理可得时间复杂度为
O(nlogn)
O
(
n
l
o
g
n
)
IDFT有两种写法,第一种是老老实实地带一个负号进去,其实还有第二种写法。
ar=1n∑rω−krA(ωk)=1n∑rω(n−k)rA(ωk)
a
r
=
1
n
∑
r
ω
−
k
r
A
(
ω
k
)
=
1
n
∑
r
ω
(
n
−
k
)
r
A
(
ω
k
)
我们把A数组反转一下,DFT之后除以n就好了。
代码实现
注意到
ωkn=e2πkin=cos(2πkn)+isin(2πkn)
ω
n
k
=
e
2
π
k
i
n
=
c
o
s
(
2
π
k
n
)
+
i
s
i
n
(
2
π
k
n
)
所以我们全程是用欧拉公式来表示
ω
ω
的
还有一点,蝴蝶操作本来我们是要迭代的,但是这里有一个优化常数的小技巧。
考虑原序列是
(a0,a1,a2,a3,a4,a5,a6,a7)
(
a
0
,
a
1
,
a
2
,
a
3
,
a
4
,
a
5
,
a
6
,
a
7
)
模拟蝴蝶变换的过程。
(a0,a2,a4,a6,a1,a3,a5,a7)
(
a
0
,
a
2
,
a
4
,
a
6
,
a
1
,
a
3
,
a
5
,
a
7
)
(a0,a4,a2,a6,a1,a5,a3,a7)
(
a
0
,
a
4
,
a
2
,
a
6
,
a
1
,
a
5
,
a
3
,
a
7
)
如果用二进制表示原序列和变换后的序列
原
(000,001,010,011,100,101,110,111)
(
000
,
001
,
010
,
011
,
100
,
101
,
110
,
111
)
后
(000,100,010,110,001,101,011,111)
(
000
,
100
,
010
,
110
,
001
,
101
,
011
,
111
)
发现其实就是二进制反转了。
然后我们就可以用递推写这个东西了,具体看代码吧。
//luoguP3803 【模板】多项式乘法(FFT)
//一个比较易于理解的版本,其实可以写得更简洁。
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 5e6 + 10;
const double pi = acos(-1.0);
struct cp {
double r, i;
cp(double _r = 0, double _i = 0) : r(_r), i(_i) {}
cp operator + (cp a) {return cp(r + a.r, i + a.i);}
cp operator - (cp a) {return cp(r - a.r, i - a.i);}
cp operator * (double a) {return cp(r * a, i * a);}
cp operator * (cp a) {return cp(r * a.r - i * a.i, r * a.i + i * a.r);}
}a[N], b[N];
int r[N], m;
void FFT(cp *F, int f) {
for(int i = 0;i < m; ++i) if(i < r[i]) swap(F[i], F[r[i]]);
for(int i = 1; i < m; i <<= 1) {
cp wn(cos(pi / i), f * sin(pi / i));
for(int j = 0;j < m; j += (i << 1)) {
cp w(1, 0);
for(int l = 0;l < i; ++l, w = w * wn) {
cp x = F[j + l], y = w * F[j + i + l];
F[j + l] = x + y; F[j + i + l] = x - y;
}
}
}
if(!~f) for(int i = 0;i < m; ++i) F[i].r /= m;
}
int main() {
int n1, n2; scanf("%d%d", &n1, &n2);
for(int i = 0, x;i <= n1; ++i) scanf("%d", &x), a[i] = cp(x, 0);
for(int i = 0, x;i <= n2; ++i) scanf("%d", &x), b[i] = cp(x, 0);
int L = 0; for(m = 1; (m <<= 1) <= (n1 + n2); ++L) ;
for(int i = 1; i < m; ++i) r[i] = (r[i >> 1] >> 1) | (i & 1) << L;
FFT(a, 1); FFT(b, 1); for(int i = 0;i < m; ++i) a[i] = a[i] * b[i];
FFT(a, -1);
for(int i = 0;i <= n1 + n2; ++i) printf("%d ", (int)(a[i].r + 0.5)); puts("");
return 0;
}
后记
参考博文
[学习笔记] 多项式与快速傅里叶变换(FFT)基础
Pick‘s Blog 里面有各种FFT系列的东西