引入:
多项式:
A
(
x
)
=
a
0
+
a
1
x
+
a
2
x
2
+
.
.
.
+
a
n
−
1
x
n
−
1
A(x)=a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1}
A(x)=a0+a1x+a2x2+...+an−1xn−1
B
(
x
)
=
b
0
+
b
1
x
+
b
2
x
2
+
.
.
.
+
b
m
−
1
x
m
−
1
B(x)=b_0+b_1x+b_2x^2+...+b_{m-1}x^{m-1}
B(x)=b0+b1x+b2x2+...+bm−1xm−1
在计算多项式的乘法,如
A
(
x
)
×
B
(
x
)
A(x)×B(x)
A(x)×B(x)时,往往需要
O
(
n
2
)
O(n^2)
O(n2)的复杂度来计算:
(平方的竖式计算)
这是因为我们用的系数表示法来存储的多项式(即把多项式的系数按顺序存在数组里)
然而,有一种更好的表示方法——点值表示法:给一个多项式带入不同的x值,得到不同的y值(如同一个函数图像,从上面取n个点的坐标)
即
y
k
=
A
(
x
k
)
(
所
有
x
k
不
相
同
,
k
=
0
,
1
,
2
,
.
.
.
,
n
−
1
)
y_k=A(x_k)(所有x_k不相同,k=0,1,2,...,n-1)
yk=A(xk)(所有xk不相同,k=0,1,2,...,n−1)
这样,我们可以固定一串
x
k
x_k
xk,用数组存储一串
y
k
y_k
yk来表示多项式。那么两个多项式相乘
A
(
x
k
)
×
B
(
x
k
)
=
A
y
k
×
B
y
k
A(x_k)×B(x_k)=A_{yk}×B_{yk}
A(xk)×B(xk)=Ayk×Byk,只需要将数组对应乘起来就可以了,
O
(
n
)
O(n)
O(n)!
然而又有问题了,好像没有人也没有题读多项式用点值表示法吧。。。我们需要把系数表示法转换为点值表示法,乘了过后,又转换回去。
如何转换?(数学老师:把点带进函数,解方程。。。好像又变回
O
(
n
2
)
O(n^2)
O(n2)了,那刚刚那个
O
(
n
)
O(n)
O(n)乘法并没有卵用)
又有一个新的圣器——快速傅里叶变换
选用一个神奇的叫单位复根的东西来作为
x
k
x_k
xk,用分治的思想
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)完成转换。
单位复根
n
n
n次单位复根即n次方为1的数,
ω
n
=
1
\omega^n=1
ωn=1的复数
ω
\omega
ω,而n次单位复根恰好有n个,分别是
e
2
π
i
k
/
n
e^{2\pi ik/n}
e2πik/n,
(
k
=
0
,
1
,
.
.
.
,
n
−
1
)
(k=0,1,...,n-1)
(k=0,1,...,n−1)。
又∵
e
i
θ
=
c
o
s
θ
+
i
s
i
n
θ
e^{i\theta}=cos\theta+i\ sin\theta
eiθ=cosθ+i sinθ
∴n个n次单位复根就会像这样呈现↓
ω
a
b
\omega_a^b
ωab就是一个半径为1的圆,转
b
a
\frac b a
ab圈的位置。
这样,单位复根就有了一些神奇的数学性质:
- ω d n d k = ω n k \omega_{dn}^{dk}=\omega_n^k ωdndk=ωnk,证明很简单,转到 d k d n \frac {dk}{dn} dndk圈就等于 k n \frac k n nk。
-
n
>
0
n>0
n>0且n为偶数,则n个n次单位复根的平方等于n/2个n/2次单位复根。
证明:
设 k ≤ n 2 k≤\frac n 2 k≤2n
( ω n k ) 2 = ω n 2 k = ω 2 n / 2 2 k = ω n / 2 k (\omega_n^k)^2=\omega_n^{2k}=\omega_{2n/2}^{2k}=\omega_{n/2}^k (ωnk)2=ωn2k=ω2n/22k=ωn/2k
( ω n n / 2 + k ) 2 = ω n n + 2 k = ω n n ω n 2 k = ( ω n k ) 2 (\omega_n^{n/2+k})^2=\omega_n^{n+2k}=\omega_n^n\omega_n^{2k}=(\omega_n^k)^2 (ωnn/2+k)2=ωnn+2k=ωnnωn2k=(ωnk)2(同上) (∵ ω n n = 1 \omega_n^n=1 ωnn=1) - 对于任意整数
n
≥
1
n≥1
n≥1和
k
k
k不为
n
n
n的倍数,即
ω
n
k
≠
1
\omega_n^k≠1
ωnk̸=1,则
Σ
j
n
−
1
(
ω
n
k
)
j
=
0
\Sigma^{n-1}_j(\omega_n^k)^j=0
Σjn−1(ωnk)j=0
证明:
这为等比数列求和,利用公式↓
Σ j n − 1 ( ω n k ) j = ( ω n k ) n − 1 ω n k − 1 = 1 k − 1 ω n k − 1 = 0 \Sigma^{n-1}_j(\omega_n^k)^j=\frac {(\omega_n^k)^n-1} {\omega_n^k -1}=\frac {1^k-1} {\omega_n^k-1}=0 Σjn−1(ωnk)j=ωnk−1(ωnk)n−1=ωnk−11k−1=0 - ω n k + n / 2 = − ω n k \omega_n^{k+n/2}=-\omega_n^k ωnk+n/2=−ωnk,相当于再 k n \frac k n nk处再转半圈,正好转到对面
FFT之Cooley-Tukey算法:分治思想。
为保证分治成功,需要将n扩大到最近的2的幂。
我们要计算
A
(
x
)
=
a
0
+
a
1
x
+
a
2
x
2
+
.
.
.
+
a
n
−
1
x
n
−
1
A(x)=a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1}
A(x)=a0+a1x+a2x2+...+an−1xn−1
的点值表示法数组y
y
i
=
A
(
ω
n
i
)
y_i=A(\omega_n^i)
yi=A(ωni)
设
A
[
0
]
(
x
)
=
a
0
+
a
2
x
+
a
4
x
2
+
.
.
.
+
a
n
−
2
x
n
/
2
−
1
A^{[0]}(x)=a_0+a_2x+a_4x^2+...+a_{n-2}x^{n/2-1}
A[0](x)=a0+a2x+a4x2+...+an−2xn/2−1
A
[
1
]
(
x
)
=
a
1
+
a
3
x
+
a
5
x
2
+
.
.
.
+
a
n
−
1
x
n
/
2
−
1
A^{[1]}(x)=a_1+a_3x+a_5x^2+...+a_{n-1}x^{n/2-1}
A[1](x)=a1+a3x+a5x2+...+an−1xn/2−1
∴
A
(
x
)
=
A
[
0
]
(
x
2
)
+
x
A
[
1
]
(
x
2
)
∴A(x)=A^{[0]}(x^2)+xA^{[1]}(x^2)
∴A(x)=A[0](x2)+xA[1](x2)
所以我们可以先分治求解数组
y
[
0
]
y^{[0]}
y[0]与
y
[
1
]
y^{[1]}
y[1]。
y
k
[
0
]
=
A
[
0
]
(
ω
n
2
k
)
=
A
[
0
]
(
ω
n
/
2
k
)
y^{[0]}_k=A^{[0]}(\omega_n^{2k})=A^{[0]}(\omega_{n/2}^k)
yk[0]=A[0](ωn2k)=A[0](ωn/2k)
y
k
[
1
]
=
A
[
1
]
(
ω
n
2
k
)
=
A
[
1
]
(
ω
n
/
2
k
)
y^{[1]}_k=A^{[1]}(\omega_n^{2k})=A^{[1]}(\omega_{n/2}^k)
yk[1]=A[1](ωn2k)=A[1](ωn/2k)
(
k
=
0
,
1
,
2...
n
2
−
1
)
(k=0,1,2...\frac n 2 -1)
(k=0,1,2...2n−1)
规模正好缩小一半。
然后我们需要用
y
[
0
]
y^{[0]}
y[0]与
y
[
1
]
y^{[1]}
y[1]来合成数组
y
y
y。
因为
k
<
n
2
k<\frac n 2
k<2n,所以
y
y
y数组需要一半一半的求。
前半部分:
y
k
=
A
(
ω
n
k
)
=
A
[
0
]
(
ω
n
/
2
k
)
+
ω
n
k
A
[
1
]
(
ω
n
/
2
k
)
=
y
k
[
0
]
+
ω
n
k
y
k
[
1
]
y_k=A(\omega_n^k)=A^{[0]}(\omega_{n/2}^k)+\omega_n^kA^{[1]}(\omega_{n/2}^k)=y^{[0]}_k+\omega_n^ky^{[1]}_k
yk=A(ωnk)=A[0](ωn/2k)+ωnkA[1](ωn/2k)=yk[0]+ωnkyk[1]
后半部分:
y
k
+
n
/
2
=
A
(
ω
n
k
+
n
/
2
)
=
A
[
0
]
(
ω
n
/
2
k
+
n
/
2
)
+
ω
n
k
+
n
/
2
A
[
1
]
(
ω
n
/
2
k
+
n
/
2
)
=
A
[
0
]
(
ω
n
/
2
k
)
−
ω
n
k
A
[
1
]
(
ω
n
/
2
k
)
=
y
k
[
0
]
−
ω
n
k
y
k
[
1
]
y_{k+n/2}=A(\omega_n^{k+n/2})=A^{[0]}(\omega_{n/2}^{k+n/2})+\omega_n^{k+n/2}A^{[1]}(\omega_{n/2}^{k+n/2})=A^{[0]}(\omega_{n/2}^k)-\omega_n^kA^{[1]}(\omega_{n/2}^k)=y^{[0]}_k-\omega_n^ky^{[1]}_k
yk+n/2=A(ωnk+n/2)=A[0](ωn/2k+n/2)+ωnk+n/2A[1](ωn/2k+n/2)=A[0](ωn/2k)−ωnkA[1](ωn/2k)=yk[0]−ωnkyk[1]
分治直到长度为1,此时
y
=
A
(
.
.
.
)
=
a
0
y=A(...)=a_0
y=A(...)=a0
然后使用上面的方法合并。
逆FFT:将点值表示法转换回系数表示法。
只需将点值表示法数组y当作系数,将
ω
n
−
k
\omega_n^{-k}
ωn−k代入计算,将结果除以n,即得到
a
k
a_k
ak。
a
k
=
y
0
+
y
1
ω
n
−
k
+
y
2
ω
n
−
2
k
+
.
.
.
+
y
n
−
1
ω
n
−
(
n
−
1
)
k
n
a_k=\frac {y_0+y_1\omega_n^{-k}+y_2\omega_n^{-2k}+...+y_{n-1}\omega_n^{-(n-1)k}} n
ak=ny0+y1ωn−k+y2ωn−2k+...+yn−1ωn−(n−1)k
又∵
y
k
=
A
(
ω
n
k
)
=
a
0
+
a
1
ω
n
k
+
a
2
ω
n
2
k
+
.
.
.
+
a
n
−
1
ω
n
(
n
−
1
)
k
y_k=A(\omega_n^k)={a_0+a_1\omega_n^{k}+a_2\omega_n^{2k}+...+a_{n-1}\omega_n^{(n-1)k}}
yk=A(ωnk)=a0+a1ωnk+a2ωn2k+...+an−1ωn(n−1)k
∴在上面分子中所有y分离出
a
i
a_i
ai单独计算:
①
i
=
k
i=k
i=k,∴可分离出:
a
i
+
a
i
ω
n
i
ω
n
−
k
+
a
i
ω
n
2
i
ω
n
−
2
k
+
.
.
.
+
a
i
ω
n
(
n
−
1
)
i
ω
n
−
(
n
−
1
)
k
=
a
i
+
a
i
+
a
i
+
.
.
.
+
a
i
=
n
a
k
a_i+a_i\omega_n^{i}\omega_n^{-k}+a_i\omega_n^{2i}\omega_n^{-2k}+...+a_i\omega_n^{(n-1)i}\omega_n^{-(n-1)k}=a_i+a_i+a_i+...+a_i=na_k
ai+aiωniωn−k+aiωn2iωn−2k+...+aiωn(n−1)iωn−(n−1)k=ai+ai+ai+...+ai=nak
②
i
≠
k
i≠k
i̸=k,每项均可分离出:
a
i
+
a
i
ω
n
i
ω
n
−
k
+
a
i
ω
n
2
i
ω
n
−
2
k
+
.
.
.
+
a
i
ω
n
(
n
−
1
)
i
ω
n
−
(
n
−
1
)
k
a_i+a_i\omega_n^{i}\omega_n^{-k}+a_i\omega_n^{2i}\omega_n^{-2k}+...+a_i\omega_n^{(n-1)i}\omega_n^{-(n-1)k}
ai+aiωniωn−k+aiωn2iωn−2k+...+aiωn(n−1)iωn−(n−1)k
=
a
i
ω
n
(
i
−
k
)
0
+
a
i
ω
n
(
i
−
k
)
+
a
i
ω
n
(
i
−
k
)
2
+
.
.
.
+
a
i
ω
n
(
i
−
k
)
(
n
−
1
)
=a_i\omega_n^{(i-k)0}+a_i\omega_n^{(i-k)}+a_i\omega_n^{(i-k)2}+...+a_i\omega_n^{(i-k)(n-1)}
=aiωn(i−k)0+aiωn(i−k)+aiωn(i−k)2+...+aiωn(i−k)(n−1)
= a i Σ j n − 1 ( ω n ( i − k ) ) j = 0 =a_i\Sigma^{n-1}_j(\omega_n^{(i-k)})^j=0 =aiΣjn−1(ωn(i−k))j=0
综上所诉,∴分子
y
0
+
y
1
ω
n
−
k
+
y
2
ω
n
−
2
k
+
.
.
.
+
y
n
−
1
ω
n
−
(
n
−
1
)
k
=
n
a
k
{y_0+y_1\omega_n^{-k}+y_2\omega_n^{-2k}+...+y_{n-1}\omega_n^{-(n-1)k}}=na_k
y0+y1ωn−k+y2ωn−2k+...+yn−1ωn−(n−1)k=nak
∴
y
0
+
y
1
ω
n
−
k
+
y
2
ω
n
−
2
k
+
.
.
.
+
y
n
−
1
ω
n
−
(
n
−
1
)
k
n
=
a
k
\frac {y_0+y_1\omega_n^{-k}+y_2\omega_n^{-2k}+...+y_{n-1}\omega_n^{-(n-1)k}} n=a_k
ny0+y1ωn−k+y2ωn−2k+...+yn−1ωn−(n−1)k=ak
逆FFT做法就是:将点值表示法数组y当作系数,将 ω n − k \omega_n^{-k} ωn−k代入计算,将结果除以n,即得到 a k a_k ak。
一个利用FFT做多项式乘积的程序。
代码:(递归版本)
#include<cstdio>
#include<cmath>
#include<cstring>
#include<complex>
using namespace std;
const int MAXN=1005;
typedef complex<double> cpxd;
cpxd out[MAXN];
void FFT(cpxd *A,cpxd *out,int n,int flag,int step=1)
/*
A是原数列
out是返回的点值表示法
n是长度
flag=1 FFT;flag=-1 逆FFT
step表示相邻两个数的距离。
因为FFT不停的提出奇偶项递归,则此时递归的数列在原数列A中,相邻两个数的距离为step
比如1,2,3,4,5,6,7,8 -> 1,3,5,7 -> 1,5 (这两个数在原数列中距离为4)
*/
{
if(n<1)return;
if(n==1)
{out[0]=A[0];return;}
FFT(A,out,n/2,flag,step*2);
FFT(A+step,out+n/2,n/2,flag,step*2);
for(int i=0;i<n/2;i++)
{
cpxd temp=exp(cpxd(0,flag*acos(-1)*2.0*i/n))*out[n/2+i];
//exp那一坨生成单位复根
out[n/2+i]=out[i]-temp;
out[i]=out[i]+temp;
}
if(flag==-1&&step==1)
for(int i=0;i<n;i++)
out[i]/=n;
}
void mul(cpxd *A,cpxd *B,cpxd *C,int n)
{
int len=1;
n<<=1;
while(len<n)len<<=1;
FFT(A,out,len,1);
memcpy(A,out,sizeof(cpxd)*len);
FFT(B,out,len,1);
memcpy(B,out,sizeof(cpxd)*len);
for(int i=0;i<len;i++)
C[i]=A[i]*B[i];
FFT(C,out,len,-1);
memcpy(C,out,sizeof(cpxd)*len);
}
int main()
{
int n;
scanf("%d",&n);
cpxd a[MAXN],b[MAXN],c[MAXN];
double x;
for(int i=1;i<=n;i++)
{
scanf("%lf",&x);
a[i]=cpxd(x,0.0);
}
for(int i=1;i<=n;i++)
{
scanf("%lf",&x);
b[i]=cpxd(x,0.0);
}
mul(a+1,b+1,c+1,n);
for(int i=1;i<2*n-1;i++)
printf("%lf ",c[i].real());
printf("%lf\n",c[2*n-1].real());
return 0;
}
/*
3
1 2 3
3 2 1
*/
/*
3.000000 8.000000 14.000000 8.000000 3.000000
*/
再来改为非递归版本,速度更快
看看递归版本如何迭代的:
如果我们把初始的数组排成最后的样子,就不需要递归了。
观察最后的数字的二进制
0 4 2 6 1 5 3 7
000 100 010 110 001 101 011 111
正好是把数字倒过来按顺序排列
代码:(非递归版本)
#include<cstdio>
#include<cmath>
#include<complex>
using namespace std;
const int MAXN=1005;
typedef complex<double> cpxd;
void FFT(cpxd *A,int n,int flag)
{
for(int i=0,j=0;i<n;i++)
{
if(j>i)swap(A[i],A[j]);//排序
int k=n;
while(j&(k>>=1))
j^=k;
j|=k;
//把二进制数字倒过来后,生成下一个数
}
double pi=flag*acos(0)*2.0;
for(int i=1;i<n;i<<=1)//每次递归的长度的一半
{
double tmp=pi/i;//因为i为长度的一半,所以÷i就已经乘以了2
for(int j=0;j<i;j++)
{
cpxd w=exp(cpxd(0,tmp*j));//生成单位复根
for(int l=j,r;l<n;l+=(i<<1))//计算每一组长度为2i的数列,一次合并
{
r=l+i;
cpxd temp=w*A[r];
A[r]=A[l]-temp;
A[l]=A[l]+temp;
}
}
}
if(flag==-1)
for(int i=0;i<n;i++)
A[i]/=n;
}
void mul(cpxd *A,cpxd *B,cpxd *C,int n)
{
int len=1;
n<<=1;
while(len<n)len<<=1;
FFT(A,len,1);
FFT(B,len,1);
for(int i=0;i<len;i++)
C[i]=A[i]*B[i];
FFT(C,len,-1);
}
int main()
{
int n;
scanf("%d",&n);
cpxd a[MAXN],b[MAXN],c[MAXN];
double x;
for(int i=1;i<=n;i++)
{
scanf("%lf",&x);
a[i]=cpxd(x,0.0);
}
for(int i=1;i<=n;i++)
{
scanf("%lf",&x);
b[i]=cpxd(x,0.0);
}
mul(a+1,b+1,c+1,n);
for(int i=1;i<2*n-1;i++)
printf("%lf ",c[i].real());
printf("%lf\n",c[2*n-1].real());
return 0;
}
/*
3
1玩
2 3
3 2 1
*/
/*
3.000000 8.000000 14.000000 8.000000 3.000000
*/
完