FFT(快速傅里叶变换)概述

表示才上高一根本不知道复数是啥,所以复数是啥???

所以这篇博客是教你在不懂复数的情况下教你怎么写FFT。

所以FFT是什么?

众所周知,要求两个多项式的乘积,时间复杂度是 n2 n 2 的。FFT可以让我们在 nlogn n l o g n 的时间里求多项式乘积(虽然在 n n 很小的情况下,FFT真的干不过n2)。

现在介绍一下思想。一个 n n 次多项式可以表示为n+1个点,也就是多项式的另一种表示方式(点值表达式)。为什么能表示成这种方式呢?建议去翻一翻数学书。。。不过感性理解一下两点确定一条直线,三点确定一个二次函数。所以 n+1 n + 1 个点好像也可以反推一个 n n 次多项式。

然后是不是有一个很直观的想法:把这两个多项式分别变成点值表达式,然后把两个点乘起来,会得到答案的一个点值。现在如果我们有答案的次数+1个点,是不是就可以反推这个多项式呢?

举个例子:

1+x1(0,1)(1,2)(2,3)

1+2x1(0,1)(1,3)(2,5) 1 + 2 ∗ x 1 — — ( 0 , 1 ) ( 1 , 3 ) ( 2 , 5 )

然后我们就得到了 (0,1)(1,6)(2,15)2x2+3x1+1 ( 0 , 1 ) ( 1 , 6 ) ( 2 , 15 ) — — 2 ∗ x 2 + 3 ∗ x 1 + 1

可以手动验证一波,不过肯定是对的。

现在的问题在于:我们如何把一个多项式迅速变成点值表达式,还有把一个点值表达式变成一个多项式。FFT就发挥了它的作用,想想我们带入的特殊值对吧,它现在没有什么性质。如果我们把它变得更有性质一些,是不是可以把程序变得更快呢?答案是当然的!

我们引入一个概念:复平面。

复平面就是可以把一个复数点变成一个向量

这里写图片描述

然后我们再学习单位根。型如 xn=1 x n = 1 在复数域内的所有根称为 x x n次单位根

举个例子: n=211 n = 2 时 ( 1 , − 1 ) n=4 n = 4 11ii ( 1 , − 1 , i , − i )

根据单位根的性质, n n 次单位正好把复平面分成n份(至于为什么我也不知道),而且单位根嘛,向量长度为1

画个图理解一下:

这里写图片描述

然后在说单位根的几个性质(我也不知道为什么)

这里写图片描述

也就是说 w w 的下标和指数是可以随意转换的。

于是我们想到一种递归的做法(函数返回一个数组,x[i]表示把 w[i] w [ i ] 带入式子的值:

void fft(E *x,int n,int type)  
{  
    if(n==1) return;  
    E l[n>>1],r[n>>1];  
    for(int i=0;i<n;i+=2)  
      l[i>>1]=x[i],r[i>>1]=x[i+1];  
    fft(l,n>>1,type);fft(r,n>>1,type);  
    E wn(cos(2*py/n),sin(type*2*py/n)),w(1,0),t;  
    for(int i=0;i<n>>1;i++,w=w*wn)  
      t=w*r[i],x[i]=l[i]+t,x[i+(n>>1)]=l[i]-t;  
}  

我们来看一看,假设递归到了这一层(因为是平分成两段,所以下一层的w[i]是这一层的 w[2i] w [ 2 i ] wn w n n n 次下的1次单位根,实际等于w[1])
然后推一波式子(假设长度为8): l[i]=a0+a2w[2i]+a4w[4i]+a6w[6i] l [ i ] = a 0 + a 2 ∗ w [ 2 i ] + a 4 ∗ w [ 4 i ] + a 6 ∗ w [ 6 i ] ;
r[i]=a1+a3w[2i]+a5w[4i]+a7w[6i] r [ i ] = a 1 + a 3 ∗ w [ 2 i ] + a 5 ∗ w [ 4 i ] + a 7 ∗ w [ 6 i ] ;

l[i]=a0+a2w2i+a4w4i+a6w6i l [ i ] = a 0 + a 2 ∗ w i 2 + a 4 ∗ w i 4 + a 6 ∗ w i 6 ;
r[i]=a1+a3w2i+a5w4i+a7w6i r [ i ] = a 1 + a 3 ∗ w i 2 + a 5 ∗ w i 4 + a 7 ∗ w i 6 ;

由于 w w 在循环里所以w=w[1]i=wi

推出: t=a1w1i+a3w3i+a5w5i+a7w7i t = a 1 ∗ w i 1 + a 3 ∗ w i 3 + a 5 ∗ w i 5 + a 7 ∗ w i 7 ;

所以当前: x[i]=l[i]+t x [ i ] = l [ i ] + t ;

然后为什么 x[i+(n>>1)]=l[i]t x [ i + ( n >> 1 ) ] = l [ i ] − t ;

假设 A(wi)=Al(wi)+Ar(wi)w1 A ( w i ) = A l ( w i ) + A r ( w i ) ∗ w 1 ;所以 A(wi+n2)=Al(wi+n2)+Ar(wi+n2w1) A ( w i + n 2 ) = A l ( w i + n 2 ) + A r ( w i + n 2 ∗ w 1 ) ;由于Al里全是平方,不取负,Ar里全是奇数次方,所以取负。

然后如何把点值表达式再变成多项式表达。这个我是真不咋会。大概是这样的,我们做一遍FFT大概就是做了一个矩阵乘法,然后我们求它的逆矩阵。经证明可得把所有 wi w i 变为 wi w − i 然后在除一个 n n 就是逆矩阵,所以可以看到上面的代码中type表示是逆向还是正向做FFT

我们现在学会FFT了然后去洛谷上交一波P3803 【模板】多项式乘法(FFT)

???嗯,怎么只有66分,我咋还re了,我咋还tle了。然后发现这个板子并不可用,真tm慢。我们可能还要学一波非递归版本。

由于我们只有在向下递归时才打乱数组顺序,所以我们不如先把数组位置排好,直接倍增向上。

这里写图片描述

位置怎么排呢?我们会发现一个很有趣的结论。在0~ n n (n是2的整次幂)的情况下, x x 的位置正好在x 2 2 进制n位下取反。那我们先把位置排好岂不美哉。

来看看代码:

#include<iostream>  
#include<cstdio>  
#include<cstring>  
#include<complex>  
#include<cmath>  
#define E complex<double>  
#define py acos(-1)  
using namespace std;  

int n,m,x,L;  
E a[2624150],b[2624150];  
int p[2624150];//请不要在意这么魔性的数字,(好吧,去掉个零是2的某整次幂,hzwer当时板子是1e5的,结果这道题变1e6了)  

void fft(E *g,int type)  
{  
    for(int i=0;i<n;i++)//把位置排好,注意这个判断,要是没有判断的话就交换两次换回去了。就只用交换小的或者大的。  
      if(i<p[i])  
        swap(g[i],g[p[i]]);  
    for(int i=1;i<n;i<<=1)//枚举小段长度  
    {  
        E wn(cos(py/i),type*sin(py/i));注意这里的py不乘2,因为我们把两个小段合成大段,所以wn应该是大段长度的单位根。而上面递归版本要乘2,因为它的段长正好是大段。总之就是2*py/i中的i一定是合并后的大段长度  
        for(int j=0;j<n;j+=2*i)//枚举每一大段  
        {  
            E w(1,0);  
            for(int k=j;k<j+i;k++,w*=wn)//枚举第一小段内的每一个元素  
            {  
                E x=g[k],y=g[k+i]*w;  
                g[k]=x+y;g[k+i]=x-y;第一小段是+,第二小段是-。  
            }  
        }  
    }  
}  

int main()  
{  
    scanf("%d%d",&n,&m);  
    for(int i=0,x;i<=n;i++) scanf("%d",&x),a[i]=x;  
    for(int i=0,x;i<=m;i++) scanf("%d",&x),b[i]=x;  
    m=n+m;for(n=1;n<=m;n<<=1) L++;  
    for(int i=0;i<n;i++) p[i]=(p[i>>1]>>1)|((i&1)<<(L-1));//这一行比较精髓,就是说我们p[i>>1]取反数已经推出。我们在它的最前面插入i的最末位就好了  
    fft(a,1);fft(b,1);  
    for(int i=0;i<=n;i++) a[i]*=b[i];  
    fft(a,-1);  
    for(int i=0;i<=m;i++) printf("%d ",(int)(a[i].real()/n+0.5));  
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值