Advertisement

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

阅读量:

文章目录

  • 问题背景

  • 前置知识

    • 1.多项式函数的表示法
    • 2.多项式的乘法
    • 3.复数
    • 4.单位根
  • 离散傅里叶转换(DFT)

    • 快速离散傅里叶转换(FFT)
    • 快速逆离散傅里叶转换(IFFT)
    • 多项式相乘代码

问题背景

无论是从信息学还是数学的角度来看,多项式都具有重要的应用价值。无论是哪种场景下,在信息学竞赛中频繁涉及利用多项式进行问题求解,并涵盖多项式的各类运算。当两个次数均为n的多项式时,在计算加减法时可以在O(n)时间内完成;相比之下,在处理乘法运算时通常需要O(n^2)的时间复杂度;然而,在所有运算的基础中尤其是快速计算方面,则对乘法提出了更高的要求

前置知识

1.多项式函数的表示法

当多项式 F 被表达成 \sum_{i=0}^na_ix^i 时,则其各a_i被称作该函数的i次项系数或简称为系数。这种表现形式被称为系数表示法。对于某个特定值x_i来说,在多项式中代入它会得到对应的输出值 y_i=F(x_i)。这样就会在平面直角坐标系中确定一个对应点(x_i,y_i)。若给定m+1个互不相同的点(x_0,y_0),(x_1,y_1),\dots,(x_m,y_m), 则可以通过代入这些点来建立m+1个方程组求解该多项式的n+1个未知系数(其中m=n)。由此可见,在这种情况下互不相同的n+1个点足以唯一确定一个n次多项式函数。

2.多项式的乘法

F(x)=\sum_{i=0}^na_ix^iG(x)=\sum_{i=0}^mb_ix^i,那么H(x)=F(x)G(x)=\sum_{i=0}^{n+m}\prod_{j+k=i}a_jb_kx^i这是一个 n+m 次多项式。
如果选取一个点 x_i,则 H(x_i)=H(x)|_{x=x_i}=[F(x)G(x)]|_{x=x_i}=F(x_i)G(x_i)也就是说把两个多项式在 x_i 处的取值相乘就可以得到乘出来的多项式在 x_i 处的取值。这样选取互不相同的 n+m+1 个点就能确定新的多项式了。

可以看出用点值表示法处理问题相较于系数表示法而言更为便捷,并且傅里叶变换即通过将系数表示转换为点值表示来实现这一过程。对于一个n次多项式F(x),我们需要选择m个特定的点来完成计算。每个点的计算所需时间为O(n),因此总的时间复杂度为O(nm)。这一复杂度与直接采用系数表示法进行相乘相当,并且由于引入了额外的常数因子而显得相对较高。传统的快速傅里叶变换是对这一算法的一种优化改进。先讲一下复数和单位根的概念

3.复数

在日常生活中,我们主要用到的是实数,而除了实数之外还有另一类数叫做虚数。实数好虚数统称为复数。
复数一般有两种表示方法。若用 c 表示一个复数,那么c=a+bi=re^{i\theta}
事实上,我们可以把一个负数和平面中一个点一一对应,那么两种表示法分别对应这平面直角坐标系和极坐标系的情况。我们称 r=\sqrt{a^2+b^2} 为模长,是一个非负数,称 \theta=\arctan\frac{b}{a} 为辐角,他是一个介于 02\pi 的数值。
加减法
假设 n=a+bi,m=c+di 为两个任意复数,则 n\pm m=(a+c)\pm(b+d)i
乘法
假设 n=a+bi,m=c+di 为两个任意复数,则 nm=(ac-bd)+(ad+bc)i。如果复数可表示为 n=re^{i\theta},m=\rho e^{i\varphi},则 nm=(r\rho)e^{i(\theta+\varphi)}。可以看出,两复数相乘,就是模长相乘,辐角相加。
乘方
类似于实数,连乘。

4.单位根

如果复数 x 满足 x^n=1,则称 xn 次单位根,记作 \omega_n,也就是说 \omega_n^n=1

在这里插入图片描述

如图所示,这是一个以 O 圆心,半径为 1 的圆,其中 A,B,\dots,H 为圆上不同的八个点。相邻的两个点之间距离相等。更具前面的知识,我们可以把任一点对应成一个复数 e^{ix\theta},其中 \theta 为点 A 所对应的辐角,k 为一个 07 之间的整数,则它的 8 次方为 e^{8ik\theta}=e^{2\pi ik}=1,也就是说,这 8 个复数都为 8 次单位根。类似地,我们可以得到 n 次单位根 \omega_n^k=e^{2\pi ik/n}(k\ 为整数)
一些性质
1.\omega_n^k=\omega_{n/2}^{k/2}
2.\omega_n^k=\omega_{n}^{k\pm n}
3.\omega_n^{k\pm \frac n 2}=-\omega_n^k
关于这些性质的证明,读者可以根据上面的图来理解。

傅里叶变换(DFT)

该算法将系数表达式转换为基于点值的表达式。具体而言,在将一个次数为n-1的多项式转换为包含n个点的点值表示时,则需将n次单位根逐一代入从而得到所需的结果。亦即该过程可表示为:对于每个k从0到n-1计算对应的函数值...] 其中函数值由a_0, a_1, \dots, a_{n-1}以及\omega_n^k各次幂之和决定。

快速傅里叶变换(FFT)

快速傅里叶变换即为傅里叶变换的一种优化方法

复制代码
    void FFT(complex *a){
    	complex w,w_n,x,y;  //复数
    	for(int i=0;i<n;i+=1){
    		if(i<rev[i]) swap(a[i],a[rev[i]]);
    	}
    	for(int i=1;i<n;i*=2){  //长度
    		w_n.real=cos(pi/i);
    		w_n.imag=sin(pi/i);  //w_n^1
    		for(int j=0;j<n;j+=i*2){   //数组中每个多项式的起始位置
    			w=1;
    			for(int k=j;k<i+j;k+=1){  //具体位置k-j才是上文的k
    				x=a[k]; y=a[k+i]*w;
    				a[k]=x+y; a[k+i]=x-y;  //(1)(2)式
    				w=w*w_n;  //求出 w_n^(k+1)
    			}
    		}
    	}
    	return;
    }

快速傅里叶逆变换(IFFT)

快速傅里叶逆变换就是把点值表达式转为系数表达式的算法,他可以由傅里叶变换的式子推导得到。考虑 b_k=\sum_{i=0}^{n-1}y_i(\omega_n^{-k})^i,把上文傅里叶变换的式子 y_k=\sum_{i=0}^{n-1}a_i(\omega_n^k)^i 代入,得到:
b_k=\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
考虑 c_j=\sum_{i=0}^{n-1}(\omega_n^{j-k})^i
j=kc_j=\sum_{i=0}^{n-1}1^i=n
j\not=k 时,不妨令 l=j-k\not=0,即 c_j=\sum_{i=0}^{n-1}(\omega_n^l)^i。将 c_j 乘上 \omega_n^l,则 \omega_n^lc_j=\sum_{i=1}^n(\omega_n^l)^i 两式相减,有 c_j=\frac{(\omega_n^l)^n-1}{\omega_n^l-1}=0b_k=\sum_{i=0}^{n-1}a_jc_j=na_k因此 a_k=\frac{b_k}{n},傅里叶逆变换的公式为 a_k=\frac{1}{n}\sum_{i=0}^{n-1}y_i(\omega_n^{-k})^i
类似于快速傅里叶变换,我们同样可以得到快速傅里叶逆变换的代码

复制代码
    void IFFT(complex *a){
    	complex w,w_n,x,y;
    	for(int i=0;i<n;i+=1){
    		if(i<rev[i]) swap(a[i],a[rev[i]]);
    	}
    	for(int i=1;i<n;i*=2){
    		w_n.real=cos(pi/i);
    		w_n.imag=-sin(pi/i);  //-k次方
    		for(int j=0;j<n;j+=i*2){
    			w=1;
    			for(int k=j;k<i+j;k+=1){
    				x=a[k]; y=a[k+i]*w;
    				a[k]=x+y; a[k+i]=x-y;
    				w=w*w_n;
    			}
    		}
    	}
    	for(int i=0;i<n;i+=1) a[i]/=n;  //别忘了除以n
    	return;
    }

多项式乘法代码

基于 FFT 和 IFFT 的实现, 我们便能够完成多项式乘法运算. 具体而言, 在对长度进行枚举时, 时间复杂度为 O(\log_2n); 而在对位置进行枚举时, 则是 O(n) 的复杂度. 因此, 整个算法的时间复杂度仅为 O(n\log_2n).

复制代码
    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<cmath>
    using namespace std;
    int n,m,len;
    int F,G;
    int rev[2500001],ans[2500001];
    double pi=acos(-1);
    struct complex{
    	double real,imag;
    	void operator=(double x){
    		real=x; imag=0.0;
    		return;
    	}
    	void write(){
    		printf("%lf+%lfi\n",real,imag);
    	}
    }f[2500001],g[2500001];
    complex operator+(complex x,complex y){
    	complex z;
    	z.real=x.real+y.real;
    	z.imag=x.imag+y.imag;
    	return z;
    }
    complex operator-(complex x,complex y){
    	complex z;
    	z.real=x.real-y.real;
    	z.imag=x.imag-y.imag;
    	return z;
    }
    complex operator*(complex x,complex y){
    	complex z;
    	z.real=x.real*y.real-x.imag*y.imag;
    	z.imag=x.real*y.imag+x.imag*y.real;
    	return z;
    }  //以上是一些关于复数的运算
    void FFT(complex *a,int flag){  //FFT/IFFT 模板
    	complex w,w_n,x,y;
    	for(int i=0;i<n;i+=1){
    		if(i<rev[i]) swap(a[i],a[rev[i]]);
    	}
    	for(int i=1;i<n;i*=2){
    		w_n.real=cos(pi/i);
    		w_n.imag=flag*sin(pi/i);
    		for(int j=0;j<n;j+=i*2){
    			w=1;
    			for(int k=j;k<i+j;k+=1){
    				x=a[k]; y=a[k+i]*w;
    				a[k]=x+y; a[k+i]=x-y;
    				w=w*w_n;
    			}
    		}
    	}
    	return;
    }
    int main(){
    	scanf("%d%d",&n,&m);
    	memset(f,0,sizeof(f));
    	memset(g,0,sizeof(g));
    	memset(rev,0,sizeof(rev));
    	for(int i=0;i<=n;i+=1) scanf("%d",&F),f[i]=F;
    	for(int i=0;i<=m;i+=1) scanf("%d",&G),g[i]=G;
    	len=n=n+m; m=1;
    	while((1<<m)<=n) m+=1; n=(1<<m);
    	for(int i=1;i<n;i+=1){
    		rev[i]=((rev[i>>1]>>1)|((i&1)<<(m-1)));
    	}  //这些都是预处理
    	FFT(f,1); FFT(g,1);
    	for(int i=0;i<=n;i+=1) f[i]=g[i]*f[i];  //逐点相乘
    	FFT(f,-1);
    	for(int i=0;i<=len;i+=1){
    		printf("%.0lf ",(f[i].real+0.5)/n);  //系数为整数,保留整数位
    	}
    	printf("\n");
    	return 0;
    }

谢谢观看,记得点赞

全部评论 (0)

还没有任何评论哟~