背板子.jpg

快速傅里叶变换(FFT) 学习笔记

特别鸣谢

GGN2015’s Blog

以及

Refun

GaryStack

什么是FFT

什么是多项式乘法

现在加入我们有两个多项式:

$f(x)=a1x^2+b1x+c1$

$g(x)=a2x^2+b2x+c2$

那么如果暴力把这两个多项式相乘,就是lena*lenb的复杂度,也就是O(n^2)的

那FFT呢

我们可以先理解为,FFT是一种可以把O(n^2)的多项式乘法转化为O(n log n)的算法

前置知识

系数表示法和点值表示法

系数表示法就是我们平时用的表示法,用n+1个系数表示一个n次函数

$f(x)=a0+a1x+a2x^2+..+anx^n ⇔ f(x) = (a0,a1,a2,..,an) $

点值表示法就是用n+1个点来表示一个n次函数

然后通过高斯消元解方程组可以得到原函数

DFT 和 IDFT

把一个多项式转化为离散的点的方法就叫做DFT(离散傅里叶变换)

把一堆离散的点还原回多项式的方法就叫做IDFT(离散傅里叶反变换)

FFT(快速傅里叶变换)就是一种用来计算DFT和IDFT的快速算法

虚数

相对于“实数”的概念

如果说$\sqrt{1}$是一个实数单位的话,那么我们定义$\sqrt{-1}$是一个虚数单位

复数

首先咱们定义一个平面坐标体系,叫做复平面

这个复平面以实数部为x轴,以虚数部作为y轴,平面上任意个点称作极坐标

设某个极坐标为(a,b),与原点的连线和x轴的夹角为θ,长度为r

那么这个点除了(a,b)还可以用a+bi或(r,θ)表示,这就是一个复数

我们可以认为,两个复数相乘有特殊的几何意义,即

$(r1,θ1)(r2,θ2)=(r1r2,θ1+θ2)$

单位复根

如果我们有两个用点值表示的多项式

$f(x)={(x_0,f(x_0)),(x_1,f(x_1)),(x_2,f(x_2)),..,(x_n,f(x_n))}$

$g(x)={(x_0,g(x_0)),(x_1,g(x_1)),(x_2,g(x_2)),…,(x_n,g(x_n))}$

那么这两个多项式相乘就是

$f(x)\times g(x)={(x_0,f(x_0)g(x_0)),(x_1,f(x_1)g(x_1)),(x_0,f(x_2)g(x_2)),..,(x_n,f(x_n)g(x_n))}$

染鹅

这样复杂度贼TM高啊

但是我们可以通过找到一个x值,使得我们不用运算n次方操作。

那么实数就有1和-1,虚数就有i和-i

可这只有4个啊

我们考虑复数,在复平面上,以原点为圆心,画一个半径为1的圆,那么圆上的点相乘时,r永远等于1,只是 θ的大小在发生改变,而这个圆上有无穷个点能满足它的某次方等于1。

我们把这种数叫做“复根”,用ω表示,如果$ω^k=1$,那么我们称“ω为1的k次复根”计做“$ω_k^n$”

其中n就是单位复根的n次方咯——那么,$ω_k^1$就是单位复根本身了

当然你可能会问这玩意怎么用程序写出来——这就要看代码了

FFT的实现

DFT

运用了分治的思想来求解,分治多项式的“奇数次项”和“偶数次项”。

$f(x)=a_0+a_1x+a_2x^2+…+a_7x^7$

$f(x)=(a_0+a_2x^2+…+a_6x^6)+x*(a_1+a_3x^2+…+a_7x^6)$

那么令

$g(x)=(a_0+a_2x+…+a_6x^3)$

$h(x)=(a_1+a_3x+…+a_7x^3)$

$f(x)=g(x^2)+x*h(x^2)$

这个二分也是有局限性的,就是它的长度不一定是2的整数倍,我们就先把它补成2的整数倍的长度,当然,最高项的系数为0。

我们还可以对这个分治的过程继续优化,因为如果多项式足够长,递归的过程要消耗大量的内存,我们就可以先在原数组内“拆分”,然后用一种类似倍增的方法计算多项式的值。

我们先看原先的分治过程是怎样的

$a_0,a_1,a_2,a_3,…,a_7$

$(a_0,a_2,a_4,a_6),(a_1,a_3,a_5,a_7)$

$(a_0,a_4),(a_2,a_6),(a_1,a_5),(a_3,a_7)$

$a_0,a_4,a_2,a_6,a_1,a_5,a_3,a_7$

我们把原数组和分治之后的对比

$a_0,a_1,a_2,a_3,a_4,a_5,a_6,a_7$

$a_0,a_4,a_2,a_6,a_1,a_5,a_3,a_7$

这样还看不出什么来,可如果我们把下标转换成二进制呢?

$a_{000},a_{001},a_{010},a_{011},a_{100},a_{101},a_{110},a_{111}$

$a_{000},a_{100},a_{010},a_{110},a_{001},a_{101},a_{011},a_{111}$

仔细一看,这不就是把二进制位从左到右翻转过来吗!

这里就不证明了……

IDFT

这个东西……意会一下吧

笔者实在是难以详尽地理解或者描述IDFT的流程 推荐Blog

GGN2015

列一下公式

我们已知 $e^{iπ}=-1$

则 $e^{2iπ}=1$

$w_k=e^{2iπ/k}$

$1/w_k=e^{2i*(-π)/k}$

然后结合底下的代码理解一下

代码实现

#include<iostream>
#include<cstdio>
#include<cmath>
#define N (4000000+100)
#define pi acos(-1)
using namespace std;
int n,m,fn,len,r[N];
struct cop {
	double x,y;
	cop (double xx=0,double yy=0) {
		x=xx; y=yy;
	}
}a[N],b[N];

cop operator + (cop a,cop b) {return cop(a.x+b.x,a.y+b.y);}
cop operator - (cop a,cop b) {return cop(a.x-b.x,a.y-b.y);}
cop operator * (cop a,cop b) {return cop(a.x*b.x-a.y*b.y,a.x*b.y+a.y*b.x);}
cop operator / (cop a,double b) {return cop(a.x/b,a.y/b);} 

void FFT(int n,cop *a,int opt) {
	for (int i=0; i<n; i++) if (i<r[i]) swap(a[i],a[r[i]]);
	for (int k=1; k<n; k<<=1) { //当前分治段的长度 
		cop wn=cop(cos(pi/k),opt*sin(pi/k));
		for (int i=0; i<n; i+=(k<<1)) { //当前分治段的起点 
			cop w=cop(1,0);
			for (int j=0; j<k; j++,w=w*wn) { //枚举段内元素 
				cop x=a[i+j],y=w*a[i+j+k];
				a[i+j]=x+y; a[i+j+k]=x-y; 
			}
		}
	}
 	if (opt==-1) for (int i=0; i<n; i++) a[i]=a[i]/n;
} 

int main() {
	scanf("%d%d",&n,&m);
	for (int i=0; i<=n; i++) scanf("%lf",&a[i].x);
	for (int i=0; i<=m; i++) scanf("%lf",&b[i].x);
	
	fn=1;
	while (fn<=n+m) fn<<=1,len++;
	for (int i=0; i<fn; i++) 
		r[i]=(r[i>>1]>>1) | ((i&1)<<(len-1));
	
	FFT(fn,a,1); FFT(fn,b,1);
	for (int i=0; i<=fn; i++) a[i]=a[i]*b[i];
	FFT(fn,a,-1);
	
	for (int i=0; i<=n+m; i++) 
		printf("%d ",(int)(a[i].x+0.5));
}

注意事项

假如说我有一个点值表达的多项式,我能不能直接用IDFT把它转化成系数表达式?

不能!

因为IDFT是在DFT的基础上进行的操作,也就是说IDFT中的“点值”是虚数形式的特殊点值,换成正常的点值表达式来操作是不会正确的。

赞 赏
蒟蒻的邮箱:xmzl200201@126.com