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

本文介绍了快速傅立叶变换(FFT)的基本原理及其在多项式乘法中的应用。通过将多项式转换为点值表达式,FFT能在nlogn的时间复杂度内完成计算,显著提升了效率。文章详细阐述了复平面、单位根等概念,并提供了实现代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

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

所以FFT是什么?

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

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

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

举个例子:

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

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

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

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

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

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

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

这里写图片描述

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

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

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

画个图理解一下:

这里写图片描述

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

这里写图片描述

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

于是我们想到一种递归的做法(函数返回一个数组,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[2i]wnwnnn次下的1次单位根,实际等于w[1])
然后推一波式子(假设长度为8):l[i]=a0+a2w[2i]+a4w[4i]+a6w[6i]l[i]=a0+a2∗w[2i]+a4∗w[4i]+a6∗w[6i];
r[i]=a1+a3w[2i]+a5w[4i]+a7w[6i]r[i]=a1+a3∗w[2i]+a5∗w[4i]+a7∗w[6i];

l[i]=a0+a2w2i+a4w4i+a6w6il[i]=a0+a2∗wi2+a4∗wi4+a6∗wi6;
r[i]=a1+a3w2i+a5w4i+a7w6ir[i]=a1+a3∗wi2+a5∗wi4+a7∗wi6;

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

推出:t=a1w1i+a3w3i+a5w5i+a7w7it=a1∗wi1+a3∗wi3+a5∗wi5+a7∗wi7;

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

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

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

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

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

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

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

这里写图片描述

位置怎么排呢?我们会发现一个很有趣的结论。在0~nn(n是2的整次幂)的情况下,xx的位置正好在x22进制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、付费专栏及课程。

余额充值