FFT(Fast Fourier Transform)
快速傅里叶变换
引入
这里的很多的工程上的文章和学术性的文章都是以音频和图像的处理和生活中的实际用途还有纯数学角度讲的,但是作为一个OIer,我们一般是不会用到这些的,所以这里就不讲解什么时域频域转换和波的分析之类的东西,而是将如何在OI中应用。
概念与前置
人物了解
其实这里要讲的傅里叶变换和完整的傅里叶变换有一定的区别,我们下面来看看傅里叶变换的两个公式:
傅里叶变换
傅里叶逆变换
其中的代表傅里叶变换,表示傅里叶逆变换。
下面扯一些和算法并无多大联系的知识。
如果不想看直接可以调到下一个标题
这两个变换公式中,为以为周期的周期函数,且还满足狄利赫里条件。
狄利赫里条件
内容概括:也就是在给定的周期函数在有限的区间内,只有有限个第一类间断点和有限个极大值和极小值。满足该条件就符合傅里叶级数收敛。
感觉每次学啥都是递归似的学习啊QWQ
傅里叶级数
内容概括:任何周期函数都可以用正弦函数和余弦函数构成的无穷级数来表示(选择正弦函数与余弦函数作为基函数是因为它们是正交的),后世称傅里叶级数为一种特殊的三角级数,根据欧拉公式,三角函数又能化成指数形式,也称傅立叶级数为一种指数级数。
- 傅里叶级数的收敛
设是周期为且在上的可积或绝对可积的函数,如果在处存在倒数,或是有两个有限的单侧倒数:
那么的级数在处收敛于,如果在处仅有两个有限的广义单侧导数:
那么的级数在处收敛于.
傅里叶级数的三角式:
其中,为函数周期。和分别控制了正弦波的振幅与频率。说好的不讲这些的呢?
它还可以用复指数形式和积分形式来表示:
其中的就是周期函数的傅里叶级数(Fourier Series, FS)
再回到傅里叶变换的两个式子,里面的称为的象函数,而被称为的原象函数。
象函数与原象函数
拉普拉斯变换
内容概述:这是一种线性的积分变换,可以将一个有参数实数的函数变换成一个参数为复数的函数。
在拉普拉斯变换中对于的双边拉普拉斯变换,我们称其中的为的象函数,而则为的原象函数。
跳出递归
下面开始由最简单的入手一步步讲解
多项式
- 定义
这个应该来看的人都知道吧
我们把形如的式子称作多项式(其中可以形式化的写作)。
其中的~叫做多项式的系数,我们将一个关于的多项式可以简记为。
多项式的通式表达:
一些规定:
我们令为一个多项式的次数界,即次数范围,那么这个多项式中的任意一项的次数都要小于。
我们定义两个多项式的加减法如下(两个多项式加起来的次数界去较大的一个):
这个可以的简单计算啦。
- 我们接下来定义两个多项式的卷积(好像也就是乘法吧):
两个多项式的卷积的次数界为。
卷积式子也可以写成以下形式:
那么从定义式的角度来看,朴素的算法只能的计算,那么有没有更快一点的计算呢?我们接着看。
多项式的表达方法
- 第一种:系数表达法
也就是我们上面的定义中的方法,用一组系数~来表示一个多项式,我们可以将这个系数看作一个维的向量,记作 ,然后向量一般可以与矩阵联系起来(后面有用)。
- 第二种:点值表达法
因为一个多项式的实质是关于的一个函数,而一个次的函数可以由个点对确定其形式形态,所以我们可以运用这个性质来表示一个多项式。对于一个次数界为的多项式,我们可以选取个,记作,然后带入这个多项式,可以对应求出个,我们把它记作,那么现在我们就得到了这个函数(多项式)上的个点对,我们就可以用这个点对来表示这个多项式。其实这也是离散化的表示一个函数(多项式)的方法。
然后我们将其形式化:
令
这个即为的点值表达式。
我们用这个点值表达式有什么用处呢?
点值表达式的方便之处在于我们将两个多项式相乘如果用系数表达式则是的,而使用点值表达式,则只用把两个多项式对应的点相乘即可,即变为的了。(这里的点相乘会用到复数的知识)。
复数
复数其实就是一种数域的扩展,为了解决一些实数解决不了的和平面几何等的问题。
于是上面的点便可以定义为复数域上的复数。
那么对于一个点,我们可以将其表示为,其中就是复数单位,其中满足。
那么这个点的四则运算就可以定义如下(结合复数的运算):
对于两个点,其中,我们可以用来表示点。
加法
减法
乘法
除法
共轭复数【百度百科】
复数就介绍到这,我们的点值表达式中的点的运算就是如此(把点用复数表示了(复平面))。
多项式求值
那么我们对于一个如何快速求出呢?
会的大佬可以跳过 Orz
先观察定义式(次数界为):
最笨的方法,每次暴力计算。
优化一点就是,利用快速幂。
再好一点的就是预处理,然后计算。
那么有没有极致一点的直接的方法呢?
我们就是用秦九韶算法(Horner法则)。
其实他的本质类似于分治递归处理。
我们将这个多项式展开:
每次提取一个,然后一直这么做下去,就得到如下变换。
的到最里面一层就是
我们发现变成了一个一次的式子,那么将带入,求出值为,继续得到如下式子:
然后同样计算,直到最后一个,那么每次都是一个一次的式子,的计算,然后总的复杂度就是啦。
代码实现就很简单啦
int v=a[n-1];
for(int i=n-2;i>=0;i--){
v=v*x+a[i];
}
接下来我们就继续看多项式卷积,我们有如下卷积式子:
表示为点值表达式就有
我们发现这样可以的计算(前面的点值表达方式说过)。
但虽然计算过程中复杂度十分优秀,但是预处理呢?
我们一般是得到一个系数表达式,然后转换为点值表达式的朴素复杂度为,先枚举个,然后每次的计算多项式的值。然后我们的运算,但大多数时候最终的答案要为系数表达式,所以最朴素的转变回来的方式就是高斯消元(也就是解一个知道个点的次方程)。
这个,,,,还不如暴力呢,emmmm。
那么有没有一种快速的变换来解决这种预处理,当然有啦,那就是——————————-> FFT
继续前置
插值-点值转换
我们继续定义一种对于多项式的运算,名叫(其实有的读者就应该知道这就是 离散傅里叶变换)
它的作用就是将系数表达式的转换为点值表达式。
而我们同样的还要定义它的逆运算,为,作用为将一个点值表达式转换为系数表达式的转换。
那么多项式的卷积就可以如下计算:
最后的答案就为了。
但是如何快速进行与呢?肯定不能用朴素的方法。
所以这里就要运用复数的神奇性质啦!
单位根
一种十分神奇且超级特殊的一类方程的根。
定义: 即对于满足方程的,被称为次单位根。
但从表面来看,为奇数时只有1,偶数时有,这样的根太少,且用处不大,那么我们就将目光投向复数。
重要的性质 : ,,
那么在复数上,就几乎有无数多组解,那么这些解就被称为次单位复数根。
因为有着的性质,那么将其带入多项式,所有的都可以变为1,那不就好解多啦。
如何求得复数意义下的解呢?
- 欧拉公式
其中一个特殊的性质:
证明:用三角恒等式,变形,然后推出在奇数和偶数时的通式证明即可。
那么欧拉公式如何得到的呢?
证明:
通过展开得到(有高数基础的应该知道,不知道的也可以百度):
然后我们将其扩展到复数域上,由于
所以可以得到如下式子:
变形得到:
于是得到证明。
如果我们将用来代替,就可以得到
即一个神奇的公式就是
然后我们接着将
那么又能够得到,那么这里的不就正好符合我们的方程吗?
然后我们令,然后我们记,那么这个就是一个次单位复数根。
而且可以证明这样的次单位复数根一定有个:
证明:
令,则
有因为有个,那么这个根也就有个。
记这些根为~,且它们在乘法意义下构成一个群,即满足。
还有一个特殊的性质是
证明:
三个引理
消去引理
当时,有
证明:折半引理
当且为偶数,,用文字描述就是个次单位根的平方的集合就等于个次单位根的集合。
证明:求和引理
对于任意的,都有
证明:用等比数列的和的公式,带入推导化简一下就出来了(公式太难打了就读者自己推一推吧QAQ)。
特殊的:
那么运用复数和单位根,可以将与降到的复杂度,下面会继续详解方法。
马上接近正题了,加油!
蝴蝶变换
其实也叫算法,它是将算法选取为基,(其实基可以在数据的大小内任意选,但是一般选来变换),然后来做变换,也就是按照下标的进制来进行分治,它一般具有的复杂度,但是只能处理大小为的整倍数的数据。
例如我们以为基,来将一个多项式的系数分成两个集合。
令集合大小为,那么就有:(其中的系数只是表示下标被分治的标志)。
那么就可以使用蝴蝶变换来神奇的实现该操作,具体形式如下图:
其实我这个图画的不好,(mspaint好难用QAQ)。
我在这就不多说了,蝴蝶逆变换也就是将改为就是逆变换了。
逆变换的证明可以看这里超级好的大佬文章,用矩阵的方法证明。
那么代码上也就简单啦!
离散傅里叶变换
其实还有连续的傅里叶变换
利用前面的算法,我们一般将基设置为,那么就是按照多项式的下标的奇偶性分类(前面提出过),所以我们记原多项式为,它被分成两个(偶,奇),我们可以将其写成如下形式:
然后根据折半引理
我们将单位根带入,可知所有的,那么直接用系数与单位根就可以计算了,并且根据折半引理,可以每次按奇偶折半分治下去,复杂度为,每次变换,所以总的时间复杂度为。
所以对于,我们有:
还有一个性质就是
又因为
所以原式就为
所以另一半也就可以求出了。
那么这个问题就较好的解决了,在的时间内。
离散傅里叶逆变换
就是的最后一步了(上一步转换为点值表达式后还要O(n)的对于项乘起来,然后在对于乘起来后的点值表达式再进行IDFT)。
IDFT它的实质就是解一个线性方程组,但不会像高斯消元一样,具体的说明可以看前面推荐过的这篇文章,因为数学公式太难打了
具体实现
根据蝴蝶变换和折半引理,分治递归处理是最直接的,但是有时效率较差,那么我们深入探究蝴蝶变换的过程发现,可以用迭代的方法,而基为的变换,实质就是基于二进制位,但是数组顺序会在变换后打乱,所以我们要提前翻转一下,下面看代码,代码中有些解释。
#include<cmath>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define db double
using namespace std;
const int M=2e6+1e5+10;
const db pi=acos(-1);
struct complex{
db x,y;
complex(db a=0,db b=0):x(a),y(b){}
complex operator +(complex a){return complex(x+a.x,y+a.y);}
complex operator -(complex a){return complex(x-a.x,y-a.y);}
complex operator *(complex a){return complex(x*a.x-y*a.y,x*a.y+y*a.x);}
//定义复数类,也可使用自带的complex,但是速度会比手写的慢。
}a[M],b[M],w[M];
int r[M],len,lg;
void DFT(complex *a,int n,int f){
// for(int i=1,j=(n>>1),k;i<=n-2;i++){
// if(i<j)swap(a[i],a[j]);
// for(k=(n>>1);j>=k;j-=k,k>>=1);
// if(j<k)j+=k;
// }每次重新变换
for(int i=0;i<n;i++) if(i<r[i]) swap(a[i],a[r[i]]);//预处理变换后直接交换
for(int i=2;i<=n;i<<=1){
int now=i>>1;
complex wn=w[i];wn.y*=f;
//预处理了单位根,如果为逆变换,f=-1,然后将sin项乘以-1
//不预处理的话精度误差会大一点,每次算的话,wn=complex(cos(2*pi/i),f*sin(2*pi/i))
for(int j=0;j<n;j+=i){
complex w=complex(1,0),x,y;
for(int k=j;k<j+now;k++,w=w*wn){
x=a[k],y=w*a[k+now];//枚举奇数项和偶数项,然后奇数项乘以单位根的次方
a[k]=x+y;a[k+now]=x-y;//根据前面推导的式子进行加减
}
}
}
if(f==-1)for(int i=0;i<n;i++)a[i].x/=n;//其实逆变换每次除以2,可以简单写为最后除以n=2^s
}
void FFT(complex *a,complex *b,int n,int m){
DFT(a,len,1);DFT(b,len,1);//先将两个多项式转换为点值表达式
for(int i=0;i<=len;i++) a[i]=a[i]*b[i];//对应项相乘
DFT(a,len,-1);//逆变换回来
for(int i=0;i<=n+m;i++) printf("%d ",int(a[i].x+0.5));
//输出系数,要注意精度误差
}
int x,n,m;
int main(){
//输入两个0~n和0~m的多项式系数,然后求其卷积后输出
//洛谷FFT模板
scanf("%d%d",&n,&m);
for(int i=0;i<=n;i++) scanf("%d",&x),a[i].x=x;
for(int i=0;i<=m;i++) scanf("%d",&x),b[i].x=x;
for(len=1;len<=n+m;len<<=1,++lg);
for(int i=0;i<len;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(lg-1));//预处理变换
for(int i=2;i<=len;i<<=1)w[i]=complex(cos(2*pi/i),sin(2*pi/i));//预处理单位根
FFT(a,b,n,m);
return 0;
}
结束与例题
类似FFT的还有:
NTT(快速数论变换):可以解决FFT的精度问题,用于在取模意义下的卷积。
FWT(快速沃尔什变换):解决集合卷积的快速算法,【我的讲解】
FMT(快速莫比乌斯变换):和莫比乌斯反演有点关系,我也不清楚,好像也可以解决一些奇怪的集合卷积问题。
一些题目:
如果有错的地方请指出,Thanks♪(・ω・)ノ。