FFT算法本身就是一种优化,优化(类似)卷积运算的时间复杂度
(卷积:
∑
i
,
j
a
i
∗
b
j
−
i
\sum_{i,j}a_i*b_{j-i}
∑i,jai∗bj−i)。
FFT的本质,其实是利用复数的一些特殊性质,将一个多项式快速地在点值和系数两种表示方法间来回切换。再利用两个多项式点值表示法相乘的复杂度为O(n),来达到降时间的目的。
FFT算法的前导概念
首先介绍关于复数的一些定义及性质
复数:一种形如
a
+
b
i
a+bi
a+bi的数称为复数,其中
a
a
a为实部,
b
b
b为虚部,
i
i
i为虚部单位
(
i
2
=
−
1
)
(i^2=-1)
(i2=−1)
对于每个 a + b i a+bi a+bi, a − b i a-bi a−bi为它的共轭复数
其实可以将复数看作一个二项式,其运算法则与实数概念上的二项式几乎相同,只需要注意i即可。
另外,复数也可以用向量来表示:表示为在复平面上(a,b)的向量。向量的模长为
a
2
+
b
2
\sqrt{a^2+b^2}
a2+b2。
所以,同样地,复数还可以用三角函数来表示:即
a
+
b
i
=
a
2
+
b
2
∗
(
c
o
s
θ
+
s
i
n
θ
i
)
a+bi=\sqrt{a^2+b^2}*(cos\theta+sin\theta i)
a+bi=a2+b2∗(cosθ+sinθi)
在这种表示方法下,两个复数相乘,就可以表示为其角度相加,模长相乘
p
∗
(
c
o
s
α
+
s
i
n
α
i
)
∗
q
∗
(
c
o
s
β
+
s
i
n
β
i
)
p*(cos\alpha+sin\alpha i)*q*(cos\beta+sin\beta i)
p∗(cosα+sinαi)∗q∗(cosβ+sinβi)
=
p
∗
q
∗
(
s
i
n
α
∗
c
o
s
β
i
+
c
o
s
α
∗
s
i
n
β
i
−
s
i
n
α
∗
s
i
n
β
+
c
o
s
α
∗
c
o
s
β
)
=p*q*(sin\alpha*cos\beta i+cos\alpha*sin\beta i-sin\alpha*sin\beta+cos\alpha*cos\beta)
=p∗q∗(sinα∗cosβi+cosα∗sinβi−sinα∗sinβ+cosα∗cosβ)
=
p
∗
q
∗
(
c
o
s
(
α
+
β
)
+
s
i
n
(
α
+
β
)
i
)
=p*q*(cos(\alpha+\beta)+sin(\alpha+\beta)i)
=p∗q∗(cos(α+β)+sin(α+β)i)
然后是多项式的一些概念:
首先我们将
A
(
x
)
=
a
n
x
n
+
a
n
−
1
x
n
−
1
+
a
n
−
2
x
n
−
2
.
.
.
a
1
x
+
a
0
A(x)=a_nx^n+a_{n-1}x^{n-1}+a_{n-2}x^{n-2}...a_1x+a_0
A(x)=anxn+an−1xn−1+an−2xn−2...a1x+a0称为一个多项式,同时这种形式也就是系数表示法。
点值表示法:
我们可以将n-1次(包括常数一共n项)的多项式,用位于A(x)上的n个点来表示。
很容易就会发现,点值表示时,两多项式相乘复杂度为O(n)
FFT算法介绍
首先还需要引入一个单位复根的概念:
单位复根就是
ω
n
=
1
\omega^n=1
ωn=1的n个解
设
ω
n
k
=
c
o
s
θ
+
s
i
n
θ
i
,
θ
=
2
k
π
n
(
k
=
0
,
1
,
2...
n
−
1
)
\omega^k_n=cos\theta+sin\theta i,\theta=\frac {2kπ} n(k=0,1,2...n-1)
ωnk=cosθ+sinθi,θ=n2kπ(k=0,1,2...n−1)
很容易发现:
ω
n
k
=
ω
n
k
+
n
\omega^k_n=\omega^{k+n}_n
ωnk=ωnk+n即有周期性
离散傅里叶变换
下面开始玄学推理:
首先
1
n
∑
k
=
0
n
−
1
ω
n
v
k
=
[
v
m
o
d
n
=
0
]
\frac {1} {n}\sum_{k=0}^{n-1}\omega_n^{vk}=[v mod n =0]
n1∑k=0n−1ωnvk=[vmodn=0]
将v换为p+q
[
(
p
+
q
)
m
o
d
n
=
r
]
=
[
(
p
+
q
−
r
)
=
0
]
=
1
n
∑
k
=
0
n
−
1
ω
n
(
p
+
q
−
r
)
k
=
1
n
∑
k
=
0
n
−
1
ω
n
p
k
ω
n
q
k
ω
n
−
r
k
[(p+q) mod n =r]=[(p+q-r)=0]=\frac {1} {n}\sum_{k=0}^{n-1}\omega_n^{(p+q-r)k}=\frac {1} {n}\sum_{k=0}^{n-1}\omega_n^{pk}\omega_n^{qk}\omega_n^{-rk}
[(p+q)modn=r]=[(p+q−r)=0]=n1∑k=0n−1ωn(p+q−r)k=n1∑k=0n−1ωnpkωnqkωn−rk
设
c
r
c_r
cr为多项式相乘后第i项的系数
有:
c
r
=
∑
p
,
q
[
(
p
+
q
)
m
o
d
n
=
r
]
a
p
b
q
=
1
n
∑
k
=
0
n
−
1
ω
n
p
k
ω
n
q
k
ω
n
−
r
k
a
p
b
q
=
1
n
∑
k
=
0
n
−
1
ω
n
−
r
k
ω
n
p
k
a
p
ω
n
q
k
b
q
c_r=\sum_{p,q}[(p+q) mod\space n =r]a_pb_q=\frac {1} {n}\sum_{k=0}^{n-1}\omega_n^{pk}\omega_n^{qk}\omega_n^{-rk}a_pb_q=\frac {1} {n}\sum_{k=0}^{n-1}\omega_n^{-rk}\omega_n^{pk}a_p\omega_n^{qk}b_q
cr=∑p,q[(p+q)mod n=r]apbq=n1∑k=0n−1ωnpkωnqkωn−rkapbq=n1∑k=0n−1ωn−rkωnpkapωnqkbq
现在,令
d
m
=
∑
k
=
0
n
−
1
ω
n
m
k
a
k
d_m=\sum_{k=0}^{n-1}\omega_n^{mk}a_k
dm=∑k=0n−1ωnmkak
g
m
=
∑
k
=
0
n
−
1
ω
n
m
k
b
k
g_m=\sum_{k=0}^{n-1}\omega_n^{mk}b_k
gm=∑k=0n−1ωnmkbk
h
m
=
d
m
×
g
m
h_m=d_m × g_m
hm=dm×gm
则
c
m
=
1
n
∑
k
=
0
n
−
1
ω
−
m
k
h
k
c_m=\frac 1 n\sum_{k=0}^{n-1}\omega^{-mk}h_k
cm=n1∑k=0n−1ω−mkhk
将a,b转化为d,e以及将h转化为c,都是离散傅里叶变换,其复杂度均为
O
(
n
2
)
O(n^2)
O(n2)。
快速傅里叶变换
现在我们要降低离散傅里叶变换的时间复杂度。
一个很简单的思路是这样的:
首先只针对于n为2的整次幂的情况
将A(x)按照奇偶次项分离,记为
A
0
(
x
)
,
A
1
(
x
)
A_0(x),A_1(x)
A0(x),A1(x)
对于任意
m
<
n
2
m<\frac n 2
m<2n
A
(
ω
n
m
)
=
A
0
(
ω
n
2
m
)
+
ω
n
m
A
1
(
ω
n
2
m
)
=
A
0
(
ω
n
/
2
m
)
+
ω
n
m
A
1
(
ω
n
2
m
)
A(\omega_n^m)=A_0(\omega^{2m}_n)+\omega_n^mA_1(\omega_n^{2m})=A_0(\omega_{n/2}^m)+\omega_n^mA_1(\omega^{2m}_n)
A(ωnm)=A0(ωn2m)+ωnmA1(ωn2m)=A0(ωn/2m)+ωnmA1(ωn2m)
A
(
ω
n
m
+
n
/
2
)
=
A
0
(
ω
n
2
m
)
+
ω
n
m
+
n
/
2
A
1
(
ω
n
2
m
)
=
A
0
(
ω
n
/
2
m
)
−
ω
n
m
A
1
(
ω
n
2
m
)
A(\omega_n^{m+n/2})=A_0(\omega^{2m}_n)+\omega_n^{m+n/2}A_1(\omega_n^{2m})=A_0(\omega_{n/2}^m)-\omega_n^mA_1(\omega^{2m}_n)
A(ωnm+n/2)=A0(ωn2m)+ωnm+n/2A1(ωn2m)=A0(ωn/2m)−ωnmA1(ωn2m)
这样一来,我们就可以通过二分来计算离散傅里叶变换。这就是快速傅里叶变换
代码实现
首先是比较浅显易懂,与讲解结合较紧密的递归版本
缺点是常数过大。
因此,我们需要迭代版本:
观察递归时函数的参数:
(
a
0
,
a
1
,
a
2
,
a
3
,
a
4
,
a
5
,
a
6
,
a
7
)
(a_0,a_1,a_2,a_3,a_4,a_5,a_6,a_7)
(a0,a1,a2,a3,a4,a5,a6,a7)
(
a
0
,
a
2
,
a
4
,
a
6
)
(
a
1
,
a
3
,
a
5
,
a
7
)
(a_0,a_2,a_4,a_6)(a_1,a_3,a_5,a_7)
(a0,a2,a4,a6)(a1,a3,a5,a7)
(
a
0
,
a
4
)
(
a
2
,
a
6
)
(
a
1
,
a
5
)
(
a
3
,
a
7
)
(a_0,a_4)(a_2,a_6)(a_1,a_5)(a_3,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
)
(a_0)(a_4)(a_2)(a_6)(a_1)(a_5)(a_3)(a_7)
(a0)(a4)(a2)(a6)(a1)(a5)(a3)(a7)
在二进制下,可以表示为:
000,100,010,110,001,101,011,111
如果你从前向后看,会发现其实是一个倒序进位的二进制数!
这样,就能知道每一层的参数中系数的顺序了。
迭代版的模板:
void fft(cpx *a,int f){
int i,j,k;
for(i=1,j=0;i<N-1;i++){//N为大于两多项式长度之和的最小的2的整次幂
for(int d=N;j^=d>>=1,~j&d;);
if(i<j)
swap(a[i],a[j]);
}
for(i=1;i<N;i<<=1){
cpx wn(cos(Pi/i),f*sin(Pi/i));
for(j=0;j<N;j+=i<<1){
cpx w(1,0);
for(k=0;k<i;k++,w=w*wn){
cpx x=a[j+k],y=w*a[j+k+i];
a[j+k]=x+y;
a[j+k+i]=x-y;
}
}
}
if(f==-1)
for(i=0;i<N;i++)
a[i].r/=N;
}
看到这里,建议再往回看开头,也许能加深印象
提供一道模板题:UOJ34多项式乘法