FFT入门学习

简介

FFT(快速傅里叶变换Fast Fourier Transformation)是DFT(离散傅里叶变换Discrete Fourier Transform)的快速算法,它是根据离散傅氏变换的奇、偶、虚、实等特性,对离散傅立叶变换的算法进行改进获得的。
FFT是用来进行多项式乘法运算,对于多项式加法,我们只需要将次数相同的数的系数相加即可。然而乘法会复杂很多,对于两个n-1次多项式,算出他们的乘积的复杂度为 O ( N 2 ) ,这显然不是特别优秀,所以我们的数学家就想出了FFT,可以将复杂度优化到 O ( N l o g 2 N )


预备知识:


多项式的点值表示

多项式的点值表示也就是把不同的数值带入多项式,然后算出结果。

定理:一个 n−1 次多项式在 n 个不同点的取值唯一确定了该多项式。
证明:假设命题不成立,存在两个不同的 n 1 次多项式 A ( x ) B ( x ) 满足对于任何 i [ 0 , n 1 ] 都存在 A ( x i ) = B ( x i )
C ( x ) = A ( x ) B ( x ) ,则 C 也是一个 n 1 次多项式,并且对于任何 i [ 0 , n 1 ] C ( x i ) = 0 。所以 C n 个根。
这与代数基本定理矛盾,所以证明完毕。

于是我们就有了把多项式转化为点值,然后让点值相称,最终再重新插值,求出最终的多项式这样一种思路。但是这种方法的朴素时间复杂度依旧为 O ( N 2 )


弧度制

弧度制也就是用弧度来表示角度,我们定义 π r a d = 180 °
这里介绍弧度制主要是因为C++三角函数库是使用弧度制的。


复数

复数域是目前已知最大的数域,我们知道在实数的时候 s q r t ( 1 ) 是不存在意义的,但是在复数域下, s q r t ( 1 ) = i ,并且我们对复数的定义与 i 有关,复数的通式为 a + b i


复平面

既然复数的定义是 a + b i ,那么我们将点(a,b)放在直角坐标系里面,则复数就转化为了一些点。我们让复平面上点 ( 0 , 0 ) 到点 ( a , b ) 的向量代表这个复数。则将 ( 0 , 0 ) ( a , b ) 的长度成为模长,将从x轴转到向量的角度大小成为幅角。
那么在这个复平面上,两个复数相加就代表着两个向量按照平行四边形法则相加;两个复数相乘就代表着两个向量模长相乘,幅角相加。


单位根

在复平面上以圆心为起点,以1为半径画圆。这个圆称为单位圆。那么选圆上的点有什么用呢?因为我们发现,复数相乘是模长相乘,幅角相加。那么我们选取单位圆上的点,也就保证了模长一直是1。再进行复数乘积时,就不许要考虑模长,只要相加幅角即可。
我们设 n = 2 x , x N 。那么我们将单位圆从(1,0)开始划分为n等份(令 w n 为到(0,1)的向量,称为n次单位根),则沿着逆时针方向的向量刚好对应着 w n 1 , w n 2


关于单位根的三个重要性质

1. w 2 n 2 k = w n k 这个可以根据单位圆图像的性质来证明。
2. w n k = c o s   k 2 π n + i   s i n   k 2 π n 这个根据三角函数即可
3. w n k + n 2 = w n k

w n k + n 2 = w n k w n n 2
w n n 2 = c o s   π + i   s i n   π = 1
w n k + n 2 = w n k 1 = w n k


快速傅里叶变换:


离散傅里叶变换

我们考虑用点值表示多项式 A ( x ) 。我们将n次单位根0~n-1代入多项式,得到点值向量
( A ( w n 0 ) , A ( w n 1 ) A ( w n n 1 ) ) 我们将多项式转换为点值向量的过程就称为离散傅里叶变换

按照朴素算法,时间复杂度依旧为 O ( N 2 )

我们考虑将系数下表奇偶数分组,则
A ( x ) = ( a 0 + a 2 x 2 + a 4 x 4 + a n 2 x n 2 ) + ( a 1 x + a 3 x 3 + a n 1 x n 1 )
我们令 A 1 ( x ) = ( a 0 + a 2 x + a 4 x 2 + a n 2 x n 2 1 ) , A 2 ( x ) = ( a 1 + a 3 x 2 + a n 1 x n 2 1 )
A ( x ) = A 1 ( x 2 ) + x A 2 ( x 2 )

我们设 k < n 2 A ( w n k ) = A 1 ( w n 2 k ) + w n k A 2 ( w n 2 k ) = A 1 ( w n 2 k ) + w n k A 2 ( w n 2 k )                           //利用性质1

对于另一半,即 A ( w n k + n 2 ) = A 1 ( w n 2 k w n n ) + w n k + n 2 A 2 ( w n 2 k w n n ) = A 1 ( w n 2 k ) w n k A 2 ( w n 2 k )
= A 1 ( w n 2 k ) w n k A 2 ( w n 2 k )                           //这部除了运用性质1,还用了性质2和 w n n = 1 这一显然的结论

所以,当k取遍 [ 0 , n 2 1 ] 时, k k + n 2 取遍了 [ 0 , n 1 ] ,所以只要我们知道 A 1 ( x ) A 2 ( x ) k [ 0 , n 2 1 ] w n 2 k 的点值,即可 O ( N ) 的求出 A ( x ) ,而 A 1 ( x ) A 2 ( x ) 都是比原问题范围缩小一半,所以可以使用分治求值,复杂度为 O ( N l o g 2 N ) ,分治的边界为常数。

我们采用递归法,即可完成离散傅里叶变换。


傅里叶逆变换

将点值转化为系数的过程就成为傅里叶逆变换,傅里叶逆变换的过程类似于离散福利叶变换。
我们设 ( y 0 , y 1 , y 2 , y 3 y n 1 ) ( a 0 , a 1 , a 2 , a n 1 ) 的离散傅里叶变换。

则我们再设一个向量 c   c k = i = 0 n 1 y i ( w n k ) i ,那么 c 就是以向量y为系数的多项式在 w n 0 , w n 1 w n n + 1 处的点值表示。

我们将 c 进行变形:
c k = i = 0 n 1 y i ( w n k ) i = i = 0 n 1 ( j = 0 n 1 a j ( w n i ) j ) ( w n k ) i = i = 0 n 1 ( j = 0 n 1 a j ( w n j ) i ) ( w n k ) i

= i = 0 n 1 ( j = 0 n 1 a j ( w n j ) i ( w n k ) i ) = i = 0 n 1 j = 0 n 1 a j ( w n j k ) i

= j = 0 n 1 a j ( i = 0 n 1 ( w n j k ) i )

我们考虑一个式子 S ( w n k ) = 1 + w n k + ( w n k ) 2 + + ( w n k ) n 1 显然当 k = 0 时,该式值为 n
否则根据等比数列公式, S ( w n k ) = ( w n k ) n 1 w n k 1 所以分子为0,原式为0。

继续考虑 c 的式子 c k = j = 0 n 1 a j ( i = 0 n 1 ( w n j k ) i ) = j = 0 n 1 a j S ( w n j k ) 所以当 j = k 时, a j S ( w n j k ) = n a j ,
否则 a j S ( w n j k ) = 0 ,所以 c i = n a i , a i = 1 n c i

所以用单位根的倒数代替单位根,再做一次类似离散傅里叶变换的操作,再将结果除以 n ,即为所求的系数。


代码实现:


递归实现:

由于上面离散傅里叶变换是每次将问题分成范围为原来一半的两个子问题,所以可以用递归分治实现。由于递归实现的效率并不是特别的高,所以在实际中一般不会用。所以我上网抄了个板子:

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
const int MAXN=2*1e6+10;
inline int read()
{
    char c=getchar();int x=0,f=1;
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
    return x*f;
}
const double Pi=acos(-1.0);
struct complex
{
    double x,y;
    complex (double xx=0,double yy=0){x=xx,y=yy;}
}a[MAXN],b[MAXN];
complex operator + (complex a,complex b){ return complex(a.x+b.x , a.y+b.y);}
complex operator - (complex a,complex b){ return complex(a.x-b.x , a.y-b.y);}
complex operator * (complex a,complex b){ return complex(a.x*b.x-a.y*b.y , a.x*b.y+a.y*b.x);}
void fast_fast_tle(int limit,complex *a,int type)
{
    if(limit==1) return ;//只有一个常数项
    complex a1[limit>>1],a2[limit>>1];
    for(int i=0;i<=limit;i+=2)//根据下标的奇偶性分类
        a1[i>>1]=a[i],a2[i>>1]=a[i+1];
    fast_fast_tle(limit>>1,a1,type);
    fast_fast_tle(limit>>1,a2,type);
    complex Wn=complex(cos(2.0*Pi/limit) , type*sin(2.0*Pi/limit)),w=complex(1,0);
    //Wn为单位根,w表示幂
    for(int i=0;i<(limit>>1);i++,w=w*Wn)//这里的w相当于公式中的k 
        a[i]=a1[i]+w*a2[i],
        a[i+(limit>>1)]=a1[i]-w*a2[i];//利用单位根的性质,O(1)得到另一部分 
}
int main()
{
    int N=read(),M=read();
    for(int i=0;i<=N;i++) a[i].x=read();
    for(int i=0;i<=M;i++) b[i].x=read();
    int limit=1;while(limit<=N+M) limit<<=1;
    fast_fast_tle(limit,a,1);
    fast_fast_tle(limit,b,1);
    //后面的1表示要进行的变换是什么类型
    //1表示从系数变为点值
    //-1表示从点值变为系数 
    //至于为什么这样是对的,可以参考一下c向量的推导过程, 
    for(int i=0;i<=limit;i++)
        a[i]=a[i]*b[i];
    fast_fast_tle(limit,a,-1);
    for(int i=0;i<=N+M;i++) printf("%d ",(int)(a[i].x/limit+0.5));//按照我们推倒的公式,这里还要除以n 
    return 0;
}

迭代实现:

一般的FFT都是采用迭代实现。迭代实现需要了解两个操作:二进制位翻转、蝴蝶操作。


二进制位翻转

考虑FFT递归时每个数的顺序,以及其二进制位:

  • 1               2               3               4               5               6 //数
  • 001         010           011           100           101           110 //二进制
  • 2               4               6               1               3               5
  • 4               2               6               1               5               3 //最终递归的数
  • 100         010           110           001           101           011 //最终数的二进制

发现规律,分治到边界后的下标等于原下标的二进制位翻转。所以我们要预处理出二进制翻转。

for(int i=0;i<limit;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(l-1));

蝴蝶操作

在进行递归时,我们需要用数组 a , a 1 , a 2 ,我们其实可以考虑一种办法,只开一个数组,让所有的操作原地完成。
我们用 a ( k ) a 1 的数,用 a ( k + n 2 ) a 2 的数,则我们只要用一个数t来存 w n k a ( n 2 + k ) ,那么 a ( n 2 + k ) = a ( k ) t a ( k ) = a ( k ) + t 这一过程就称为蝴蝶操作。

complex t=w*a2[i];
a[i]=a1[i]+t,a[i+(limit>>1)]=a1[i]-t;

迭代实现模板
#include<bits/stdc++.h>
#define MAXN 1000005
using namespace std;
int read(){
    char c;int x=0,y=1;while(c=getchar(),(c<'0'||c>'9')&&c!='-');
    if(c=='-') y=-1;else x=c-'0';while(c=getchar(),c>='0'&&c<='9')
    x=x*10+c-'0';return x*y;
}
const double pi=acos(-1.0);
struct comple{
    double x,y;
    comple (double xx=0,double yy=0){x=xx,y=yy;}
}a[MAXN],b[MAXN];
comple operator+(comple a,comple b){return comple(a.x+b.x,a.y+b.y);}
comple operator-(comple a,comple b){return comple(a.x-b.x,a.y-b.y);}
comple operator*(comple a,comple b){return comple(a.x*b.x-a.y*b.y,a.y*b.x+a.x*b.y);}
int n,m,limit=1,l,r[MAXN];
void FFT(comple *A,int type){
    for(int i=0;i<limit;i++) if(i<r[i]) swap(A[i],A[r[i]]);
    for(int mid=1;mid<limit;mid<<=1){
        comple Wn(cos(pi/mid),type*sin(pi/mid));
        for(int R=mid<<1,j=0;j<limit;j+=R){
            comple w(1,0);
            for(int k=0;k<mid;k++,w=w*Wn){
                comple x=A[j+k],y=w*A[j+k+mid];
                A[j+k]=x+y;A[j+k+mid]=x-y;
            }
        }
    }
}
int main()
{
    n=read();m=read();
    for(int i=0;i<=n;i++) a[i].x=read();
    for(int i=0;i<=m;i++) b[i].x=read();
    while(limit<=n+m) limit<<=1,l++;
    for(int i=0;i<limit;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(l-1));
    FFT(a,1);FFT(b,1);
    for(int i=0;i<=limit;i++) a[i]=a[i]*b[i];
    FFT(a,-1);
    for(int i=0;i<=n+m;i++) printf("%d ",(int)(a[i].x/limit+0.5));
    return 0;
}

FFT简单应用

我们来讨论FFT一个非常简单的应用,就是用FFT来实现 A B ,我们知道当 A , B 的范围为 10 2000 时我们可以直接用高精度来求解,但是如果范围更大呢?如果 A , B 的范围来到 10 10 6 呢?那么这时候高精度 N 2 模拟都会超时,但是复杂度为 N l o g 2 N 的FFT显然可以。我们考虑怎么用FFT来加速。由于FFT求的是卷积,而我们发现对于 A B 乘出的新数的第 k 位(没有进位之前)是 i = 0 k a i b k i ,这个就是一个卷积的形式,所以我们可以先用FFT来求出没有进位前的数。然后我们再手动进位,再判断一下前导 0 即可。

#include<bits/stdc++.h>
#define db double
#define MAXN 2000005
using namespace std;
int read(){
    char c;int x;while(c=getchar(),c<'0'||c>'9');x=c-'0';
    while(c=getchar(),c>='0'&&c<='9') x=x*10+c-'0';return x;    
}
const db pi=acos(-1.0);
struct Complex{
    db x,y;
    Complex (db xx=0,db yy=0){x=xx,y=yy;}
}a[MAXN],b[MAXN];
Complex operator+(Complex a,Complex b){return Complex(a.x+b.x,a.y+b.y);}
Complex operator-(Complex a,Complex b){return Complex(a.x-b.x,a.y-b.y);}
Complex operator*(Complex a,Complex b){return Complex(a.x*b.x-a.y*b.y,a.x*b.y+a.y*b.x);}
int n,m,limit=1,l,r[MAXN],top;
string s;
void init(Complex *A,int &x){
    cin>>s;x=s.length()-1;
    for(int i=0;i<s.length();i++) A[i].x=s[i]-'0';
}
void FFT(Complex *A,int type){
    for(int i=0;i<limit;i++) if(i<r[i]) swap(A[i],A[r[i]]);
    for(int mid=1;mid<limit;mid<<=1){
        Complex Wn(cos(pi/mid),type*sin(pi/mid));
        for(int R=mid<<1,j=0;j<limit;j+=R){
            Complex w(1,0);
            for(int k=0;k<mid;k++,w=w*Wn){
                Complex x=A[j+k],y=w*A[j+k+mid];
                A[j+k]=x+y;A[j+k+mid]=x-y;
            }
        }
    }
}
int main()
{
    init(a,n);init(b,m);
    while(limit<=n+m) limit<<=1,l++;
    for(int i=0;i<limit;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(l-1));
    FFT(a,1);FFT(b,1);
    for(int i=0;i<limit;i++) a[i]=a[i]*b[i];
    FFT(a,-1);
    for(int i=0;i<=n+m;i++) a[i].x=(int)(a[i].x/limit+0.5);
    for(int i=n+m;i>=0;i--){
        int x=a[i].x,r=1;a[i].x=x%10;x/=10;if(x) a[i-r].x+=x,x/=10,r++;
    }
    int flag=0;
    for(int i=-10;i<=n+m;i++){
        if(!flag&&(int)a[i].x==0) continue;
        if((int)a[i].x) flag=1;
        printf("%d",(int)a[i].x);
    }
    if(!flag) puts("0");
    return 0;
}

猜你喜欢

转载自blog.csdn.net/stevensonson/article/details/80546246
FFT