快速傅里叶变换(Fast Fourier Transform,FFT)

前言 关于为什么又开始在这个博客上写算法

FFT是我高中学竞赛阶段接触到的最后一个算法,也是我高中一直没有学会的算法之一,在高中毕业的时候认为自己大学本科一定学的是CS,所以想要把这些自己之前没有学会的算法都再琢磨一下,可是基于当时的知识储备我还完全不知道复数域和复平面以及多项式的各种定理,于是半途而废了,FFT也成了在算法学习上我的一个心结之一。
但是巧合的是,我大学本科并没有学CS,而是学了数学,这些我一直未曾学会的算法也就被一直搁置了两年,直到昨天我在上数据结构课时,数学院数据结构课所学内容跟OI比起来简直不值一提,于是无聊的我突发奇想翻开了自己之前洛谷的提交记录,发现FFT的版子还一直放在我的题单,于是就想利用这些时间把这些之前没学懂的算法解决掉好了,也为明年考研跨考CS打下基础。
这次在数学院历练了两年多的我拿着高等代数数学分析复变函数的知识再来看FFT,顿时觉得这些知识难度与我两年前看的时候相比要简单了许多,在昨天晚上解决了自己所有的疑难点实现了代码拿到了AC。
看到我的这个博客之前发布的线段树网络流等等算法讲解总浏览量都接近一万了,那就继续吧,让这个断更了4年的博客重新活起来!


奈芙莲封面是这个博客的灵魂!!

问题背景

多项式乘法
给定一个n次多项式A(x)m次多项式B(n),求出F(x)G(x)的卷积。

输入格式

第一行两个整数n,mn,m
接下来一行n+1个数字,从低到高表示F(x)的系数。
接下来一行m+1个数字,从低到高表示G(x)的系数。

输出格式

一行n+m+1个数字,从低到高表示F(x) \cdot G(x)的系数。

输入输出样例

输入样例

1 2
1 2
1 2 1

输出样例

1 4 5 2

问题分析

假设两个多项式A(x)=a_0+a_1x+a_2x^2+\cdots +a_{n-1}x_{n-1}B(x)=b_0+b_1x+b_2x^2+\cdots +b_{n-1}x^{n-1},两个多项式可以写作:
\begin{equation} A(x)=\sum_{i=0}^{n-1}a_ix^i\\ B(x)=\sum_{i=0}^{n-1}a_ix^i \end{equation}
传统方法是利用两个多项式的系数进行卷积运算,得到一个2n-2次多项式C(x)
\begin{equation} C(x)=\sum_{i=0}^{2n-2}c_ix_i \end{equation}
这种卷积运算的时间复杂度为O(n^2),显然在数据范围较大的情况下难以承受,而利用快速傅里叶变换可将时间复杂度降为O(nlogn)

FFT介绍

快速傅里叶变换 (fast Fourier transform),即利用计算机计算离散傅里叶变换(DFT)的高效、快速计算方法的统称,简称FFT。快速傅里叶变换是1965年由J.W.库利和T.W.图基提出的。采用这种算法能使计算机计算离散傅里叶变换所需要的乘法次数大为减少,特别是被变换的抽样点数N越多,FFT算法计算量的节省就越显著。
FFT(Fast Fourier Transformation) 是离散傅氏变换(DFT)的快速算法。即为快速傅氏变换。它是根据离散傅氏变换的奇、偶、虚、实等特性,对离散傅立叶变换的算法进行改进获得的。
——百度百科

要解决的问题是多项式的乘法,而一个多项式的表示方法并不唯一,传统意义上多项式的表示利用的是系数表示法,即对于一个多项式A(x)=a_0+a_1x+a_2x^2+\cdots +a_{n-1}x_{n-1}可由一个系数向量(a_0,a_1,a_2,\cdots,a_{n-1})唯一表示。

而除了系数表示法之外,多项式也可以利用点值表示,对于多项式A(x),选定nx(x_0,x_1,x_2,\cdots,x_{n-1})带入多项式进行计算,得到n个点值\{ (x_0,A(x_0)),(x_1,A(x_1)),(x_2,A(x_2)),\cdots,(x_{n-1},A(x_{n-1})) \}n个点值也可唯一表示多项式A(x)

因此当我们利用同一个向量x得到了两个两个多项式的点值表示法后,用对应点值相乘,得到(A(x_0)B(x_0),A(x_1)B(x_1),A(x_2)B(x_2),\cdots,A(x_{n-1})B(x_{n-1}))即为两个多项式的乘积多项式的点值表示,这个过程的时间复杂度为O(n)

需要注意的一点是,当两个多项式次数为n,m时,他们的乘积多项式次数为n+m,因此利用点值表示计算时,计算两个多项式的点值表示时应选用n+m+1个变量,才能使得到的结果唯一表示乘积多项式C(x)

然而对于一个多项式A(x)来说,代入任意选定的变量(x_0,x_1,x_2,\cdots,x_{n-1})计算他的点值表示法时间复杂度依然是O(n^2),并没有起到优化的效果,而快速傅里叶变换解决了这个问题,使得系数表示法转化为点值表示法的时间复杂度降低为O(nlogn)

快速傅里叶变换FFT

FFT在计算多项式的系数表示法变换为点值表示法时,选定复平面上单位圆上的单位复根\omega_n^k作为变量计算多项式的点值,在这里单位根\omega_n^k满足以下的一些性质(如果有不理解的可以自行查阅复数的一些相关知识):
\begin{aligned} \omega_n^k&=e^{\frac{2k\pi}{n}i}=\cos\frac{2k\pi}{n}+i\sin\frac{2k\pi}{n}\\ n&=2^i,i\in N^+\quad k\in [0,n)\\ \omega_n^0&=\omega_n^n=1\\ \omega_n^{\frac{n}{4}}&=i\\ \omega_n^{\frac{3n}{4}}&=-i\\ \omega_{2n}^{2k}&=\omega_{n}^k\\ \omega_{n}^{k+\frac{n}{2}}&=-\omega_n^k\\ \end{aligned}
以上的这些性质都可以由Euler公式得到,推导过程并非FFT重点这里就省略了。

对于一个多项式A(x)=a_0+a_1x+a_2x^2+\cdots +a_{n-1}x_{n-1},我们可以对其进行划分,将偶数次项与奇数次项分开,在这里假设n-1为奇数,得到:
A(x)=(a_0+a_2x^2+\cdots +a_{n-2}x^{n-2})+x(a_1+a_3x^2+\cdots +a_{n-1}x^{n-2})
我们分别定义两个多项式A_1(x),A_2(x)
\begin{aligned} &A_1(x)=a_0+a_2x^1+\cdots +a_{n-2}x^{\frac{n}{2}-1}\\ &A_2(x)=a_1+a_3x^2+\cdots +a_{n-1}x^{\frac{n}{2}-1}\\ \end{aligned}
那么原多项式A(x)就可以表示为:
A(x)=A_1(x^2)+xA_2(x^2)
\omega_n^k代入上式:
\begin{aligned} A(\omega_n^k)&=A_1(\omega_n^{2k})+\omega_n^kA_2(\omega_n^{2k})\\ &=A_1(\omega_{\frac{n}{2}}^k)+\omega_n^kA_2(\omega_{\frac{n}{2}}^k) \end{aligned}
\omega_n^{k+\frac{n}{2}}代入上式:
\begin{aligned} A(\omega_n^{k+\frac{n}{2}})&=A_1(\omega_{n}^{2k+n})+\omega_n^{k+\frac{n}{2}}A_2(\omega_n^{2k+n})\\ &=A_1(\omega_n^n\times \omega_n^{2k})-\omega_n^kA_2(\omega_n^n\times \omega_n^{2k})\\ &=A_1(\omega_n^{2k})-\omega_n^kA_2(\omega_n^{2k})\\ &=A_1(\omega_{\frac{n}{2}}^k)-\omega_n^kA_2(\omega_{\frac{n}{2}}^k) \end{aligned}
由此可以发现,A(\omega_n^k)A(\omega_n^{k+\frac{n}{2}} )在计算的过程中只有一个符号不同,因此在进行枚举计算A(\omega_n^k)时即可直接得到A(\omega_n^{k+\frac{n}{2}} )的值,利用这种方法进行分治,便可以将复杂度降至O(nlogn)

逆傅里叶变换IFFT

利用上述的方法得到了乘积多项式的点值表示法,那么现在需要解决的问题时如何将点值表示法再转换回系数表示法。

假设得到多项式的FFT点值表示为(y_0,y_1,y_2,\cdots ,y_{n-1}),其系数表示为(a_0,a_1,a_2,\cdots ,a_{n-1}),根据FFT原理,y_k可如下表示:
y_k=\sum_{i=0}^{n-1}a_i(\omega_n^k)^i
\omega_n^k共轭复数\omega_n^{-k},如下定义向量(c_0,c_1,c_2,\cdots ,c_{n-1})
c_k=\sum_{i=0}^{n-1}y_i(\omega_n^{-k})^i
那么由定义可以推导出如下的公式:
\begin{aligned} c_k&=\sum_{i=0}^{n-1}y_i(\omega_n^{-k})^i\\ &=\sum_{i=0}^{n-1}(\sum_{j=0}^{n-1}a_j(\omega_n^i)^j)(\omega_n^{-k})^i\\ &=\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}a_j(\omega_n^{j-k})^i\\ &=\sum_{j=0}^{n-1}a_j\sum_{i=0}^{n-1}(\omega_n^{j-k})^i \end{aligned}
对于复平面上的单位根\omega_n^k,有如下的性质:
\begin{aligned} \sum_{i=0}^{n-1}(\omega_n^{j-k})^i&=0\quad(j\neq k)\\ \sum_{i=0}^{n-1}(\omega_n^{j-k})^i& =\omega_n^ 0=1 \quad (j= k ) \end{aligned}
因此可以得到:
\begin{aligned} &c_k=na_k\\ &a_k=\frac{c_k}{n} \end{aligned}
因此,利用FFT得到了多项式的点值表示后只需要将变量换为原本选定的单位根的共轭复数再进行一次FFT就能得到多项式的系数表示。

这里需要说明一个问题,我们以上的讨论都是建立在n2的幂次的条件下的,那么当n不是2的幂次时需要n扩大为大于n的最小的2的幂次,在进行逆傅里叶变换时,通过上述的推导可以发现,当我们计算的c_kk的值大于原本的n时,就不存在j=k的情况了,因此求得的a_k0,表示该多项式的k次项系数为0

代码实现

下面是利用递归实现FFT的代码,具体的解释见代码注释好了。

#include<bits/stdc++.h>
#define rep(i,a,b) for(register int i=a;i<=b;++i)
using namespace std;

const int N=1e6+10;
const double Pi=acos(-1.0);
int n,m,l=1;

inline int read(){
    int x=0,f=1;
    char c=getchar();
    while(c<'0' || c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c<='9' && c>='0'){x=(x<<3)+(x<<1)+(int)(c-'0');c=getchar();}
    return x*f;
}

struct Complex{
    double re,im;
    Complex (double xx=0,double yy=0){re=xx,im=yy;}
};

Complex operator + (Complex a,Complex b){
    return Complex(a.re+b.re,a.im+b.im);
}

Complex operator - (Complex a,Complex b){
    return Complex(a.re-b.re,a.im-b.im);
}

Complex operator * (Complex a,Complex b){
    return Complex(a.re*b.re-a.im*b.im,a.im*b.re+a.re*b.im);
}

Complex a[N<<1],b[N<<1];

inline void FFT(Complex *a,int t,int inv){//inv表示当前进行FFT还是IFFT
    if(t==1)return;//计算长度为1时回溯
    int mid=t>>1;
    static Complex tmp[N];
    rep(i,0,mid-1)
        tmp[i]=a[2*i],tmp[i+mid]=a[2*i+1];//将多项式按照次数奇偶进行二分
    rep(i,0,t-1)a[i]=tmp[i];
    FFT(a,mid,inv);
    FFT(a+mid,mid,inv);
    Complex wn(cos(2.0*Pi/t),inv*sin(2.0*Pi/t)),w(1,0);//单位根
    rep(i,0,mid-1){
        tmp[i]=a[i]+w*a[i+mid];
        tmp[i+mid]=a[i]-w*a[i+mid];
        w=w*wn;
    }
    rep(i,0,t-1)a[i]=tmp[i];
}

int main(){
    n=read(),m=read();
    rep(i,0,n) a[i].re=read();
    rep(i,0,m) b[i].re=read();
    while(l<=(n+m))l<<=1;//找到大于等于n+m的最小2的幂次
    FFT(a,l,1);
    FFT(b,l,1);
    rep(i,0,l)
        a[i]=a[i]*b[i];
    FFT(a,l,-1);
    rep(i,0,n+m)
        printf("%d ",(int)(a[i].re/l+0.5));//输出整数,需要注意精度
    return 0;
}

但是问题到这里并没有结束

当有的毒瘤数据范围非常大的时候,用递归进行计算时,大量的递归会造成栈溢出,那么是否有不用递归的做法?

FFT的迭代实现

对于这样一个序列(a_0,a_1,a_2,a_3,a_4,a_5,a_6,a_7),我们观察对其进行二分的过程:


我们发现了一个神奇的性质,在对这个序列进行二分以后的序列的二进制可以由原序列的二进制进行翻转得到,那么我们可以利用这个性质,用一个O(n)的方法可以直接得到最终的序列,从而省去了递归的过程,用最终的序列反向递推实现即可。

Rader算法

Rader算法即为实现上述操作的一种算法,对于N个数,我们把递增自然数(0,1,2,3,\cdots)称为顺序数列;对顺序数列中的每一个数,将其二进制倒序后转化为十进制,称为倒序数列。
对于一个顺序数列,第i个数的二进制可以视为将第i/2(这里是整除)个数的二进制左移一位,再根据i的奇偶性对其末尾加1或者不加1
那么要得到它的倒序数列,只需要将这个操作反向进行即可,即第i个数的二进制可以视为将第i/2个数的二进制右移一位,再根据i的奇偶性对其最高加1或者不加1,这里最高位即为第\log_{2}n位。

迭代进行FFT(蝴蝶变换)

利用Rader算法求得了递推序列,那么如何通过迭代得到最终的答案?
这其实跟迭代实现01背包的做法思路差不多,对于求A(x)n次单位根的各幂次的点值时,m=n/2次单位根的各幂次在A_1A_2处的点值已经被计算并且储存在了A数组中,那么在下一层的迭代过程中直接使用A数组存储的答案继续进行迭代计算即可。

迭代优化FFT代码实现

详细解释依旧见代码注释。

#include<bits/stdc++.h>
#define rep(i,a,b) for(register int i=a;i<=b;++i)
using namespace std;

const int N=1e7+10;
const double Pi=acos(-1.0);
int n,m,l=1,t=0;
int bin[N<<1];

inline int read(){
    int x=0,f=1;
    char c=getchar();
    while(c<'0' || c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c<='9' && c>='0'){x=(x<<3)+(x<<1)+(int)(c-'0');c=getchar();}
    return x*f;
}

struct Complex{
    double re,im;
    Complex (double xx=0,double yy=0){re=xx,im=yy;}
};

Complex operator + (Complex a,Complex b){
    return Complex(a.re+b.re,a.im+b.im);
}

Complex operator - (Complex a,Complex b){
    return Complex(a.re-b.re,a.im-b.im);
}

Complex operator * (Complex a,Complex b){
    return Complex(a.re*b.re-a.im*b.im,a.im*b.re+a.re*b.im);
}

Complex a[N<<1],b[N<<1];

inline void FFT(Complex *A,int inv){
    rep(i,0,l-1)
        if(i<bin[i]) swap(A[i],A[bin[i]]);//得到倒序数列
    for(int mid=1;mid<l;mid<<=1){//枚举当前迭代区间的中点
        Complex wn(cos(Pi/mid),inv*sin(Pi/mid));//单位根
        for(int r=mid<<1,j=0;j<l;j+=r){//j枚举迭代区间位置,用r来得到区间右端点
            Complex w(1,0);
            rep(k,0,mid-1){//k枚举当前迭代区间
                Complex x=A[k+j],y=w*A[k+j+mid];
                A[k+j]=x+y;
                A[k+j+mid]=x-y;
                w=w*wn;
            }
        }
    }
}

int main(){
    n=read(),m=read();
    rep(i,0,n) a[i].re=read();
    rep(i,0,m) b[i].re=read();
    while(l<=(n+m)){
        l<<=1,t++;
    }
    rep(i,0,l)//Rader算法
        bin[i]=(bin[i>>1]>>1)|((i&1)<<(t-1));
    FFT(a,1);FFT(b,1);
    rep(i,0,l)
        a[i]=a[i]*b[i];
    FFT(a,-1);
    rep(i,0,n+m)
        printf("%d ",(int)(a[i].re/l+0.5));
    return 0;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容