快速傅立叶变换(FFT)
FFT的里有许多地方我也搞不懂,我不想懂也不需要懂,知道结论能用就行了。。。看了好多天的鬼东西,本来觉得好难,看完之后觉得也不过如此。
快速傅立叶变换FFT,是一种用来加速多项式相乘(卷积)的算法O(n*logn)。
离散傅里叶变换DFT: 传统暴力方式下实现多项式相乘O()。
对于一个多项式:F(x) = +
+
+.......+
, 当x=10 时 F (x) 的值 即是一个10进制数。也就是说高精度相乘是多项式相乘的一个特例。
傅里叶变换的大致过程:
1. 系数表示法 -> 点值表示法(称为傅里叶变换DFT)。
2. 在点值表示法下相乘,合并两个多项式为一个多项式。
3. 点值表示法->系数表示法 (称为傅里叶逆变换IDFT)。
多项式系数表示法:
形如 F(x) = +
+
+.......+
,由系数{
,
,
,...
} n个系数来表示的叫做系数表示法。
点值表示法:
(两点确定一条直线,三点确定一条抛物线,n个点确定一个n-1次多项式?好像大概应该可能是这个意思)
取n个互不相同的 x 值 { ,
,
...
} 分别带入多项式F(x) ,
得到n个点 { ( , F(
) ) , (
, F(
) ) ,(
, F(
) ) ........ (
, F(
) ) },这是它的点值表示法。
1.系数表示法 -> 点值表示法(DFT)
取n个互不相同的 x 带入多项式即取得n个点的集合就是DFT的过程。
2.点值相乘
将两个在点值表示法下的多项式,按相同位置上的两个多项式的两点相乘,得到新的多项式的这个位置上的点,最终即能合并成为一个点值表示法下的多项式。
3.点值表示法->系数表示法(IDFT)
在上述DFT的过程中动些手脚,就能实现IDFT逆变换。
以上便是大致过程,接下来是详细描述。
能够实现O(n*logn)的原因:在选取n个互不相同的点带入多项式时,多项式有n项,每个点代入求F ( x ) 的复杂度为O(n),代入n个点为O() 。而取一些特殊的点,即能实现O(n*logn)复杂度,单位复根就能满足这样的要求。
单位复根:
( 复数 a+bi , =-1 ) ,一个复数 w, 满足
=1,则称 w 是 n 次 单位根,单位根都是在半径为1的单位圆上。
(下面将会出现这样的表达形式: ,表示(复数w的n次方根)的m次方 )
旋转因子,乘一个旋转因子取得下一个复数。
以w的4次方根为旋转因子。 以w的8次方根为旋转因子。
=(1,0)
=(1,0)
=
*
=
*
=
*
=
*
=
*
=
*
=
*
...............
可以注意到:n次方根,它的幂就会有n个不同的取值,方便的让我们选择n个不同的值代入n次多项式进行点值表达式转换。
同是它还有两个比较好的性质( 观察上面两个图 ):
1. =
,比如:
=
=
..... (消去引理? 能够实现递归分治的依据)
2. = −
,比如:
= −
..... (折半引理?能将问题规模减半)
对于多项式:F(x)= +
+
+.......+
提取它的偶次项系数组成新的多项式: (x)=
+
+
+.......+
提取它的奇次项系数组成新的多项式: (x)=
+
+
+.......+
它将会有如下的性质:
F(x) = (
)+ x *
(
) ( 简单地代入
,然后合并就等于 F(x) 了 )
接下来就是需要稍微理解的东西了。
将 代入F( x ):
F()=
+
+
+ ....... +
根据上面推出的性质得:
1. F()=
(
^2 )+
(
^2 )
= (
)+
(
)
= (
)+
(
) (消去引理,
=
)
2. F()=
(
^2)+
(
^2 )
= (
^2 ) +
(
^2 ) (折半引理,
= −
)
= (
) +
(
)
= (
) −
(
) (折半引理,
= −
)
总结以上两条:想要将n个系数的多项式F(x)转换为点值表达式,先将 F(x)中的偶次项系数 和 奇次项系数 取出组合成两个新的n / 2个系数的多项式,递归分治地求解这两个子问题,再将他们合并去求解 n 个 系数的 多项式 F(x)。
递归的形式:
void FFT(complex<double> a[],int n){
if(n==1) return;
complex<double> *a0=new complex<double>[n/2];
complex<double> *a1=new complex<double>[n/2];
for(int i=0;i<n;i+=2){ //用奇偶次项系数组成新的多项式
a0[i/2]=a[i];
a1[i/2]=a[i+1];
}
FFT(a0,n/2);FFT(a1,n/2); //递归分治问题
complex<double> wn(cos(2*M_PI/n),sin(2*M_PI/n)); //取n次方根
complex<double> w(1,0);
for(int i=0;i<(n/2);i++){ //蝶形操作,由下一层的a[i],a[i+n/2]
a[i]=a0[i]+w*a1[i]; //更新本层的 a[i],a[i+n/2]
a[i+n/2]=a0[i]-w*a1[i];
w=w*wn; //旋转因子
}
return;
}
完整代码:
#include<bits/stdc++.h>
using namespace std;
int a[100];
int b[100];
void FFT(complex<double> a[],int n,int op){
if(n==1) return;
complex<double> *a0=new complex<double>[n/2];
complex<double> *a1=new complex<double>[n/2];
for(int i=0;i<n;i+=2){ //用奇偶次项系数组成新的多项式
a0[i/2]=a[i];
a1[i/2]=a[i+1];
}
FFT(a0,n/2,op);FFT(a1,n/2,op); //递归分治问题
complex<double> wn(cos(2*M_PI/n),sin(2*M_PI*op/n)); //取n次方根
complex<double> w(1,0);
for(int i=0;i<(n/2);i++){ //蝶形操作,由下一层的a[i],a[i+n/2]
a[i]=a0[i]+w*a1[i]; //更新本层的 a[i],a[i+n/2]
a[i+n/2]=a0[i]-w*a1[i];
w=w*wn; //旋转因子
}
return;
}
int main()
{
int m1,m2;
scanf("%d%d",&m1,&m2);
for(int i=0;i<m1;i++){
scanf("%d",&a[i]);
}
for(int i=0;i<m1;i++){
scanf("%d",&b[i]);
}
int n=1;
while(n<m1+m2){ //不能小于两个多项式相乘的长度m1+m2
n<<=1;
}
complex<double> *c=new complex<double> [n+1];
complex<double> *d=new complex<double> [n+1];
for(int i=0;i<m1;i++){
c[i]=complex<double>(a[i],0);
}
for(int i=0;i<m2;i++){
d[i]=complex<double>(b[i],0);
}
FFT(c,n,1); //DFT变换
FFT(d,n,1); //DFT变换
for(int i=0;i<n;i++){
c[i]=c[i]*d[i]; //点值相乘
}
FFT(c,n,-1); //IDFT逆变换
for(int i=0;i<n;i++){
a[i]=(c[i].real()/n+0.5); // n倍结果
}
for(int i=0;i<n;i++){
printf("%d ",a[i]);
}
return 0;
}
/*
测试数据,
输入:
5 5
1 1 0 0 0
1 0 1 1 1
输出:
1 1 1 2 2 1 0 0 0 0 0 0 0 0 0 0
解释:输入数据为高低位颠倒的两个数
(00011) * (11101)= (0000000000122111)
(11) * (11101)= (122111)
*/
注意:
1.复数的n次方根=( cos(2*/n) , sin(2*
/n) )。
2.蝶形操作是依据于上面的式子1,2 的结果。
3.complex<double>是属于系统库提供的复数类,效率低,有BUG,可能会产生错误。
4.每次递归都要分成两个一半的子问题,所以多项式系数个数(长度)必须严格为2的幂,不足则补零。
5.多项式长度不仅要2的幂,还不能小于两个多项式相乘的结果的长度。
6.逆变换是将单位复根的虚部变为负数( 或者......),逆变换的实部 / n,才是多项式相乘的结果。
7.大数运算要高低位颠倒,结果要进位。
迭代替代递归:
减少一次DFT变换的方式:
尽力优化double精度误差问题:
NNT快速数论变换:
FWT?: