FFT,即快速傅里叶变换,是离散傅里叶变换的快速方法,可以在很低复杂度内解决多项式乘积的问题(两个序列的卷积

卷积


卷积通俗来说就一个公式(本人觉得卷积不重要)

$$C_k=\sum_{i+j=k}A_i*B_i$$

那么这个表达式是啥意思了:

  有两个序列AB,其中$A=\left\{A_1,A_2,...\right\},B=\left\{B_1,B_2,...\right\}$

  A序列有a个元素,B序列有b个元素。于是,由这两个序列可以推出另一个序列C,C序列如何确定了?确定方法就按照卷积的公式得来的,即$$C=\left\{\sum_{i+k=0}A_i*B_j\,,\,\sum_{i+k=1}A_i*B_j\,,...,\sum_{i+k=2^a+2^b}A_i*B_j\right\}$$

这里只介绍一下卷积,下面来探究卷积和多项式之间的关系。


多项式(预备知识)


多项式的定义

形如

$$f(x)=a_0+a_1x+a_2x^2+...+a_nx^n$$

的函数关系式叫做关于x的多项式,其中多项式系数可以构成一个序列,即

$$A=\left\{a_0,a_1,a_2,...,a_n\right\}$$

多项式乘法与序列的卷积

假如有两个多项式,其中

$f(x)=a_0+a_1x+a_2x^2+...+a_nx^n$

$g(x)=b_0+b_1x+b_2x^2+...+b_mx^m$

现在要求f(x)*g(x)的序列,很明显

$$f(x)*g(x)=\sum_{i+j=0}a_i*b_j+\sum_{i+j=1}a_i*b_jx+\sum_{i+j=2}a_i*b_jx^2+...+\sum_{i+j=m+n}a_i*b_jx^{m+n}$$

现在把$f(x),g(x)$以及$f(x)*g(x)$三个多项式的系数拿出来写成三个序列,可得:

$A=\left\{a_0,a_1,a_2,...a_n\right\}$

$B=\left\{b_0,b_1,b_2,...b_m\right\}$

$C=\left\{\sum_{i+j=0}a_i*b_j,\sum_{i+j=1}a_i*b_j,\sum_{i+j=2}a_i*b_j,...\sum_{i+j=m+n}a_i*b_j\right\}$

于是惊讶的发现,两个序列的卷积相当于两个多项式乘积得到的多项式系数序列。而FFT算法的目的,就是求两个多项式乘积后得到的多项式的系数序列

多项式的表示方法

多项式有两种表示方法,即系数表示法点值表示法

1.系数表示法是我们常用的表示方法,即

$$f(x)=a_0+a_1x+a_2x^2+...+a_nx^n$$

的形式

2.点值表示法,顾名思义,就是在这个多项式上任意选取不重复的n+1个点,即

$$f(x)=\left\{(x_0,y_0),(x_1,y_1),...,(x_2,y_2)\right\}$$

可以证明:任何n+1个点可以确定唯一一个最高次项为n次方的多项式(下面是证明,可以看看,也可以忽略)

将一个多项式的系数写成一个系数矩阵(当然,这些系数我们是不知道的)

$$\begin{bmatrix}a_0\\a_1\\a_2\\\vdots\\a_n\end{bmatrix}$$

然后将刚刚选取的n+1个点写成下面两个矩阵

$$\begin{bmatrix}1&x_0&x_0^2&x_0^3&\cdots&x_0^n\\1&x_1&x_1^2&x_1^3&\cdots&x_1^n\\1&x_2&x_2^2&x_2^3&\cdots&x_2^n\\\vdots&\vdots&\vdots&\vdots&\ddots&\vdots\\1&x_n&x_n^2&x_n^3&\cdots&x_n^n\end{bmatrix}\;\begin{bmatrix}y_0\\y_1\\y_2\\\vdots\\y_n\end{bmatrix}$$

可以得出以下三个矩阵的关系

$$\begin{bmatrix}1&x_0&x_0^2&x_0^3&\cdots&x_0^n\\1&x_1&x_1^2&x_1^3&\cdots&x_1^n\\1&x_2&x_2^2&x_2^3&\cdots&x_2^n\\\vdots&\vdots&\vdots&\vdots&\ddots&\vdots\\1&x_n&x_n^2&x_n^3&\cdots&x_n^n\end{bmatrix}\;\begin{bmatrix}a_0\\a_1\\a_2\\\vdots\\a_n\end{bmatrix}=\begin{bmatrix}y_0\\y_1\\y_2\\\vdots\\y_n\end{bmatrix}$$

解出系数矩阵

$$\begin{bmatrix}a_0\\a_1\\a_2\\\vdots\\a_n\end{bmatrix}=\begin{bmatrix}1&x_0&x_0^2&x_0^3&\cdots&x_0^n\\1&x_1&x_1^2&x_1^3&\cdots&x_1^n\\1&x_2&x_2^2&x_2^3&\cdots&x_2^n\\\vdots&\vdots&\vdots&\vdots&\ddots&\vdots\\1&x_n&x_n^2&x_n^3&\cdots&x_n^n\end{bmatrix}^{-1}\begin{bmatrix}y_0\\y_1\\y_2\\\vdots\\y_n\end{bmatrix}$$

只能唯一确定一个系数矩阵,即只能唯一确定一组系数,即只能唯一确定一个多项式,证明完毕

多项式乘法与FFT算法

为了实现多项式乘法并得到系数序列,我们可以考虑实现的方法,如果直接暴力(通过定义直接算系数)是O(n2)复杂度,肯定会超时,于是我们这样考虑

首先可以选取n+1个点,把两个多项式转化为点值表示法

然后将两个点值表示法的多项式相乘(复杂度为O(n)),然后将新得到的多项式的点值表示法转化为系数表示法。

注意:假设f(x)最高有n次方,g(x)最高有m次方,所以f(x)*g(x)最高有m+n次方,但是m+n可能不是2的幂次方,如果不是,则需要选取点的数量应该是大于m+n的2的幂次方个,假设这个值为1<<k,所以从系数表示法到点值表示法的转化过程中,我们要在f(x)和g(x)内选择1<<k的点,才能保证点值表示法的f(x)和g(x)之后,至少仍有m+n个点,才能确定唯一一个f(x)*g(x)的多项式。所以在选取点的数量的时候,要保证点的个数能确定f(x)*g(x)后的多项式。

如果任意的选取n+m个点然后转化,复杂度还是太高,所以我们需要巧妙的选取1<<k个点,从而使得系数表示法与点值表示法之间转化的复杂度降为O(nlogn),这就是FFT算法。


虚数的乘积及其表示(预备知识)


一个虚数,是由实部和虚部组成,表示为$(a+bi)$,虚数的几何表示可以在坐标系中体现,如下图所示

如图所示,$r$为此虚数的模长,$\theta$为此虚数的幅角,于是虚数还能表示为$r(cos\theta,isin\theta)$的形式

若$z_1=r_1(cos\theta_1,isin\theta_1),z_2=r_2(cos\theta_2,isin\theta_2)$

于是$$z_1*z_2=r_1(cos\theta_1,isin\theta_1)*r_2(cos\theta_2,isin\theta_2)=r_1r_2(cos(\theta_1+\theta_2),isin(\theta_1+\theta_2))$$

可以得到虚数相乘的几何意义:模长相乘,幅角相加


n次单位根(预备知识)


定义

n次单位根$w_n$,即$x^n=1$在虚数范围内的解,即$w_n^n=1$,故n次单位根$w_n$也是一个复数,且模长为1所以n次单位根在乘方的时候模长不变,幅角等差增大

如图所示,将一个单位圆分成n块幅角为正最小的那个虚数即为n次单位根$w_n^1$红色点),下图是n=8的情况

还要注意,$w_n^0=w_n^n=1$

由图及n次单位根的性质(n次单位根模长为1,幅角为$\frac{2\pi}{n}$)可得

$$w_n^k=cos\frac{2k\pi}{n}+i\,sin\frac{2k\pi}{n}$$

性质

比较简单的性质:

1.$w_n^{m+n}=w_n^m*w_n^n=w_n^m$

2.$(w_n^m)^n=(w_n^n)^m=1,m\in [0,n-1]$

比较复杂(重要)的性质:

1.$w_{2n}^{2k}=w_n^k$,如图所示

2.$w_n^k=-w_n^{k+\frac n{2}}$

通过上面那个图也可以看出来,注意一点:虚数乘-1,那么虚数的实部与虚部都要乘上-1,所以$-w_n^k$在单位圆上的点和$w_n^k$在单位圆上的点关于原点对称

3.$(w_n^k)^2=w_{\frac n{2}}^k$

推导:$(w_n^k)^2=(-w_n^{k+\frac n{2}})^2=w_n^{2*k}=w_{\frac n{2}}^k$

更加复杂(重要)的性质

$$\sum_{k=0}^{n-1}w_n^{ik}=\begin{cases} 0 & i\bmod n \ne 0 \\ n & i\bmod n=0 \end{cases}$$

证明:

  当$i\bmod n \ne 0$时,相当如等比数列前n项和求和,故$$\sum_{k=0}^{n-1}w_n^{ik}=\frac {w_n^0(1-(w_n^i)^n)}{1-w_n^i}=\frac {1-(w_n^n)^i}{1-w_n^i}=0$$

  当$i\bmod n = 0$时,$w_n^{ik}=1$,故$$\sum_{k=0}^{n-1}w_n^{ik}=n*1=n$$

至此,你应该知道,多项式从系数表示法转化为点值表示法时,选取的n个点的x值即为n次单位根序列$\left\{w_n^0,w_n^1,....,w_n^{n-1}\right\}$

注意,此处的n是f(x)*g(x)的多项式最高次项的次数,不是f(x)或者g(x)最高此项的系数,前面(多项式乘法与FFT)讲过要选取1<<k(再次强调这个值比f(x)*g(x)的多项式最高次的次数m+n还要大,且是2的幂次方)个点


FFT


假设现在多项式为

$f(x)=a_0+a_1x+a_2x^2+...+a_{k-1}x^{k-1}$,注意,多项式最高次项为k-1次方,共k项,k如果不等于2的n次幂,就凑成2的n次幂(加系数为0的项)

$g(x)=b_0+b_1x+b_2x^2+...+b_{h-1}x^{h-1}$,注意,多项式最高次项为h-1次方,共h项,h如果不等于2的n次幂,就凑成2的n次幂(加系数为0的项)

那么现在,我们从多项式中选取了这样一些点

${(w_n^0,f(w_n^0)),(w_n^1,f(w_n^1)),(w_n^2,f(w_n^2)),...,(w_n^{n-1},f(w_n^{n-1}))}$,共n个点

注意n是大于k+h的最小2次幂,而不是等于k+h!!!!!

时选取得点数n大于k+h,故可以确定f(x)*g(x)这个多项式,现在,只需确定$f(w_n^0),f(w_n^1),...,f(w_n^{n-1})$的值即可。

如何算值?我们可以观察一下$f(w_n^i)$的值$(0\le i<n)$

$f(w_n^i)=a_0+a_1w_n^i+a_2(w_n^i)^2+a_3(w_n^i)^3+...+a_{k-1}(w_n^i)^{k-1}$

$=\color{Red}{a_0+a_2(w_n^i)^2+a_4(w_n^i)^4+...+a_{k-2}(w_n^i)^{k-2}}+\color{LimeGreen}{w_n^i}\color{Blue}{(a_1+a_3(w_n^i)^2+...+a_{k-1}(w_n^i)^{k-2})}$

根据$(w_n^k)^2=w_{\frac n{2}}^k$可得

$=\color{Red}{a_0+a_2w_{\frac n{2}}^i+a_4(w_{\frac n{2}}^i)^2+...+a_{k-2}(w_{\frac n{2}}^i)^{\frac {k-2}2}}+\color{LimeGreen}{w_n^i}\color{Blue}{(a_1+a_3w_{\frac n{2}}^i+...+a_{k-1}(w_{\frac n{2}}^i)^{\frac {k-2}2})}$

$=\color{Red}{f_1(w_n^i)}+\color{LimeGreen}{w_n^i}\color{Blue}{f_2(w_n^i)}$

再算$f_1(w_n^i)$的值和$f_2(w_n^i)$的值的时候,我们又可以按照奇偶分开,像算$f(w_n^i)$一样变成两个问题

同时我们发现

$f(w_n^{i+\frac n{2}})=f(-w_n^i)$

$=\color{Red}{a_0+a_2(-w_n^i)^2+a_4(-w_n^i)^4+...+a_{k-2}(-w_n^i)^{k-2}}-\color{LimeGreen}{w_n^i}\color{Blue}{(a_1+a_3(-w_n^i)^2+...+a_{k-1}(-w_n^i)^{k-2})}$

$=\color{Red}{a_0+a_2w_{\frac n{2}}^i+a_4(w_{\frac n{2}}^i)^2+...+a_{k-2}(w_{\frac n{2}}^i)^{\frac {k-2}2}}-\color{LimeGreen}{w_n^i}\color{Blue}{(a_1+a_3w_{\frac n{2}}^i+...+a_{k-1}(w_{\frac n{2}}^i)^{\frac {k-2}2})}$

$=\color{Red}{f_1(w_n^i)}-\color{LimeGreen}{w_n^i}\color{Blue}{f_2(w_n^i)}$

又发现,如果我们要算$f(w_n^i)$的值,我们会先算$f_1(w_n^i)$和$f_2(w_n^i)$的值,但是算出$f_1(w_n^i)$和$f_2(w_n^i)$的值之后,不仅能算出$f(w_n^i)$的值,还能同时算出$f(w_n^{i+\frac n{2}})$的值

总的来说,如果n=8算出了$f(w_n^0)$的值,就算出了$f(w_n^4)$的值;算出了$f(w_n^1)$,就算出来$f(w_n^5)$的值;...;算出了$f(w_n^3)$的值,就算出了$f(w_n^7)$的1值

同理,算$f_1(w_n^i)$和$f_2(w_n^i)$的值的时候,也是算出一半,得到另一半的值------每次解决问题,都会变成解决一半问题然后直接得到另一半的答案,在解决这一半问题的时候,也变成了解决一半的一半的问题,另一半的一半的问题又被直接得到答案

这样,就可以把$f(w_n^0),f(w_n^1),f(w_n^2),...,f(w_n^{n-1})$的值全部算出来,得到了n个点的值;同理算出$g(w_n^0),g(w_n^1),f(w_n^2),...,g(w_n^{n-1})$的值。得到了f(x)和g(x)的点值表示法,点值表示法相乘,O(n)复杂度就得到了f(x)*g(x)的点值表示法

代码如下:

#include <complex>//c++自带复数模板
typedef complex<double> Complex;//重定义数据类型,记得要用double类型
void FFT(Complex *a,int n,int inv){
//a是你要进行变换的数组(多项式系数数组),n是数组大小,inv暂时不管,你就当做它等于1
if(n==) return;//如果分治后的序列大小为1,就直接返回,不用继续分治了
int mid=n>>;//如果n不等于1,那就继续分治,mid是分治后变成两个序列的长度
static Complex b[];//创建一个辅助用的b数组,后面体现用处
for(int i=;i<mid;i++) b[i]=a[*i],b[i+mid]=a[*i+];//将a数组奇数位置的值和偶数位置的值分开,b数组前mid个位置存a数组偶数位置值,后mid个位置存a数组奇数位置值
for(int i=;i<n;i++) a[i]=b[i];//将b数组值重新赋给a数组 //对分治后的两个序列(一个是a数组前mid个元素组成的序列,另一个数a数组后mid元素组成的序列) 进行FFT变换
FFT(a,mid,inv);
FFT(a+mid,mid,inv); //算出一半,得到另一半的值,a数组前mid元素相当于之前说的f1,a数组后mid元素相当于之前说的f2
for(int i=;i<mid;i++){
Complex com(cos(*pi*i/n),inv*sin(*pi*i/n));
b[i]=a[i]+com*a[i+mid];b[i+mid]=a[i]-com*a[i+mid];
} for(int i=;i<n;i++) a[i]=b[i];//重新将b数组赋值给a数组
return;
}

FFT变换(递归版)

这里再来详细解释一下,上面代码中的n值

加入开始两个序列是n次多项式和m次多项式,那么开始n值就等于大于等于$(m+n)$的最小2次幂,如下代码所示

cin>>n>>m;
int k=n+m,lim=;
while(k>>=) lim<<=;
if(lim<n+m) lim<<=;
FFT(a,lim,);//对a序列进行变换
FFT(b,lim,);//对b序列进行变换

此时lim值就是n的初值

强调一遍$$w_n^k=cos\frac{2k\pi}{n}+i\,sin\frac{2k\pi}{n}$$


FFT逆变换


FFT逆变换则是将点值表示法转化为系数表示法的步骤。在逆变换之前,我们已经对两个多项式系数序列进行了正变换,即

FFT(a,lim,);//正变换
FFT(b,lim,);
for(int i=0;i<lim;i++) a[i]=a[i]*b[i];//将正变换的两个序列乘在一起,得到新的多项式的点值表示法序列。

如上所示,只要将a序列进行逆变换,就可以得到新多项式的系数序列。在FFT正变换之中,我们一直在求$f(w_n^i)$的值。

假设所求多项式系数序列为$k_0,k_1,...k_{n-1}$

现在对于新的多项式a(新得到的序列,不是原来的a序列)来求卷积$$c_h=\sum_{i=0}^{n-1}a_i(w_n^{-h})^i$$

相当于对a序列又进行FFT变换,只不过,我们现在以新序列a为系数的多项式在$x=w_n^{-i}$的一系列值

相当于求$C(x)=a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1}$在$x=w_n^0,w_n^-1,w_n^{-2},...w_n^{-(n-1)}$的点值表示法

而我们知道$a_x=k_0+k_1w_n^x+k_2(w_n^k)^2+...=\sum_{j=0}^{n-1}k_j(w_n^x)^j$

$$\sum_{i=0}^{n-1}a_i(w_n^{-h})^i$$

$$=\sum_{i=0}^{n-1}(\sum_{j=0}^{n-1}k_j*(w_n^i)^j)(w_n^{-h})^i$$

$$=\sum_{j=0}^{n-1}k_j(\sum_{i=0}^{n-1}(w_n^{j-h})^i)$$

$$=\sum_{j=0}^{n-1}k_j(\sum_{i=0}^{n-1}w_n^{(j-h)*i})$$

由于当且只当$j=h$时,$(j-h)$才为i的倍数,其余时候为0,根据n次单位根更加复杂的性质可得(不知道的看看n次单位根预备性质

$$\sum_{j=0}^{n-1}k_j(\sum_{i=0}^{n-1}w_n^{(j-h)*i})$$

$$=n*k_h$$

所以

$$k_h=\frac {\sum_{i=0}^{n-1}a_i(w_n^{-h})^i}{h}=\frac {c_h}{n}$$

说明:$w_n^{-i}$与$w_n^i$实部相同,虚部相反。现在知道代码中inv的作用了吧

当inv=1,表示FFT变换;当inv=-1,表示FFT逆变换

FFT(a,lim,);
FFT(b,lim,);
for(int i=;i<lim;i++) a[i]=a[i]*b[i];
FFT(a,lim,-1);
//经过FFT逆变换之后,a序列虚部都已经为0,只有实部有值,且为整数

for(int i=;i<n+m-;i++) printf("%d ",(int)(a[i].real()/lim+0.5));//实部根据前面推导要除以n(这里除以lim)

为啥要加0.5捏:首先我们确定答案肯定是整数,但是在代码实现过程中,由于有$\pi$介入计算,导致答案可能变小了(误差很小),所以加一个0.5,然后强制类型转换切掉小数部

比如说本来答案是1,但是代码实现的结果可能是0.999999999999,此时加上了0.5,然后转换为int,答案就是1了。

强调一遍$$w_n^k=cos\frac{2k\pi}{n}+i\,sin\frac{2k\pi}{n}$$

FFT代码

#include <complex>//c++自带复数模板
typedef complex<double> Complex;//重定义数据类型,记得要用double类型
void FFT(Complex *a,int n,int inv){
//a是你要进行变换的数组(多项式系数数组),n是数组大小,inv=1表示正变换,inv=-1表示逆变换
if(n==) return;//如果分治后的序列大小为1,就直接返回,不用继续分治了
int mid=n>>;//如果n不等于1,那就继续分治,mid是分治后变成两个序列的长度
static Complex b[];//创建一个辅助用的b数组,后面体现用处
for(int i=;i<mid;i++) b[i]=a[*i],b[i+mid]=a[*i+];//将a数组奇数位置的值和偶数位置的值分开,b数组前mid个位置存a数组偶数位置值,后mid个位置存a数组奇数位置值
for(int i=;i<n;i++) a[i]=b[i];//将b数组值重新赋给a数组 //对分治后的两个序列(一个是a数组前mid个元素组成的序列,另一个数a数组后mid元素组成的序列) 进行FFT变换
FFT(a,mid,inv);
FFT(a+mid,mid,inv); //算出一半,得到另一半的值,a数组前mid元素相当于之前说的f1,a数组后mid元素相当于之前说的f2
for(int i=;i<mid;i++){
Complex com(cos(*pi*i/n),inv*sin(*pi*i/n));
b[i]=a[i]+com*a[i+mid];b[i+mid]=a[i]-com*a[i+mid];
} for(int i=;i<n;i++) a[i]=b[i];//重新将b数组赋值给a数组
return;
}

FFT变换


FFT系数储存注意


在进行FFT之前,我们有两个多项式系数序列,用数组存系数的不要用普通的数组存,而要用复数数组,因为在计算的时候系数涉及复数计算,所以这样

#include <complex>//c++自带复数模板
Complex a[],b[]; for(int i=;i<n;i++) scanf("%d",&t),a[i]=Complex(t,);//构造函数
for(int i=;i<m;i++) scanf("%d",&t),b[i]=Complex(t,);

这样,复数的实部存系数的值,虚部为0

一个算多项式相乘后系数序列的程序

//开头说明一下,最下面有输入输出样例 

#include <iostream>

#include <cmath>
#define pi acos(-1) #include <complex>//c++自带复数模板
using namespace std;
typedef complex<double> Complex;//重定义数据类型,记得要用double类型 Complex a[],b[];
int n,m; void FFT(Complex *a,int n,int inv){
//a是你要进行变换的数组(多项式系数数组),n是数组大小,inv=1表示正变换,inv=-1表示逆变换
if(n==) return;//如果分治后的序列大小为1,就直接返回,不用继续分治了
int mid=n>>;//如果n不等于1,那就继续分治,mid是分治后变成两个序列的长度
static Complex b[];//创建一个辅助用的b数组,后面体现用处
for(int i=;i<mid;i++) b[i]=a[*i],b[i+mid]=a[*i+];//将a数组奇数位置的值和偶数位置的值分开,b数组前mid个位置存a数组偶数位置值,后mid个位置存a数组奇数位置值
for(int i=;i<n;i++) a[i]=b[i];//将b数组值重新赋给a数组 //对分治后的两个序列(一个是a数组前mid个元素组成的序列,另一个数a数组后mid元素组成的序列) 进行FFT变换
FFT(a,mid,inv);
FFT(a+mid,mid,inv); //算出一半,得到另一半的值,a数组前mid元素相当于之前说的f1,a数组后mid元素相当于之前说的f2
for(int i=;i<mid;i++){
Complex com(cos(*pi*i/n),inv*sin(*pi*i/n));
b[i]=a[i]+com*a[i+mid];b[i+mid]=a[i]-com*a[i+mid];
} for(int i=;i<n;i++) a[i]=b[i];//重新将b数组赋值给a数组
return;
}
int main(){
cin>>n>>m;//输入两个多项式的项数
int k=n+m,lim=,t;
for(int i=;i<n;i++) scanf("%d",&t),a[i]=Complex(t,);//第一个多项式系数
for(int i=;i<m;i++) scanf("%d",&t),b[i]=Complex(t,);//第二个多项式系数
while(k>>=) lim<<=;
if(lim<n+m) lim<<=;
FFT(a,lim,);
FFT(b,lim,);
for(int i=;i<lim;i++) a[i]=a[i]*b[i];
FFT(a,lim,-);
for(int i=;i<n+m-;i++) printf("%d ",(int)(a[i].real()/lim+0.5));
}
/* //输入
2 3//第一个多项式有两项 ,第二个多项式有三项
1 2//第一个多项式为(1+2x)
2 1 2//第二个多项式为(2+x+2x^2) //输出
2 5 4 4//(1+2x)*(2+x+2x^2)=2+5x+4x^2+4x^3,系数为2 5 4 4 */

多项式相乘

测试用例

//输入
//两个多项式都有两项
//第一个多项式为(1+x)
//第二个多项式为(1+x)
//输出
//(1+x)*(1+x)=1+2*x+x^2,系数为1 2 1

测试


FFT变换的示例演示


在讲FFT迭代版本之前,我打算先用一个例子来展示FFT变换,加深认识

假设

  多项式$f(x)=3+2x+x^2$

  多项式$g(x)=2+x+2x^2$

在草稿纸上算一下,这两个多项式相乘的结果为$h(x)=6+7x+10x^2+5x^3+2x^4$

先得到$f(x)$与$g(x)$多项式的系数序列,注意应该写成复数形式的序列(复数形式为(real,image))

  多项式$f(x)$的系数序列为$A=\left\{(3,0),(2,0),(1,0)\right\}$

  多项式$g(x)$的系数序列为$B=\left\{(2,0),(1,0),(2,0)\right\}$

然后判断两个多项式相乘后有多少项,应该不超过3+3=6项。而大于6的最小2次幂应该为8,所以要把两个多项式序列补成8项。

(用(0,0)来补项)

  多项式$f(x)$的系数序列为$A=\left\{(3,0),(2,0),(1,0),(0,0),(0,0),(0,0),(0,0),(0,0)\right\}$

  多项式$g(x)$的系数序列为$B=\left\{(2,0),(1,0),(2,0),(0,0),(0,0),(0,0),(0,0),(0,0)\right\}$

对于每一个$f(w_8^i)$,可以寻得规律

$f(w_8^i)=A_0+A_1w_8^i+A_2(w_8^i)^2+A_3(w_8^i)^3+A_4(w_8^i)^4+A_5(w_8^i)^5+A_6(w_8^i)^6+A_7(w_8^i)^7$

$=A_0+A_2(w_8^i)^2+A_4(w_8^i)^4+A_6(w_8^i)^6+w_8^i(A_1+A_3(w_8^i)^2+A_5(w_8^i)^4+A_7(w_8^i)^6)$

$=A_0+A_2w_4^i+A_4(w_4^i)^2+A_6(w_4^i)^3+w_8^i(A_1+A_3w_4^i+A_5(w_4^i)^2+A_7(w_4^i)^3)$

$=A_0+A_4(w_4^i)^2+w_4^i(A_2+A_6(w_4^i)^2)+w_8^i(A_1+A_5(w_4^i)^2+w_4^i(A_3+A_7(w_4^i)^2))$

$=A_0+A_4w_2^i+w_4^i(A_2+A_6w_2^i)+w_8^i(A_1+A_5w_2^i+w_4^i(A_3+A_7w_2^i))$

然后

$f(w_8^{i+4})=f(-w_8^i)=A_0+A_1(-w_8^i)+A_2(-w_8^i)^2+A_3(-w_8^i)^3+A_4(-w_8^i)^4+A_5(-w_8^i)^5+A_6(-w_8^i)^6+A_7(-w_8^i)^7$

$=A_0+A_2(w_8^i)^2+A_4(w_8^i)^4+A_6(w_8^i)^6-w_8^i(A_1+A_3(w_8^i)^2+A_5(w_8^i)^4+A_7(w_8^i)^6)$

只要算出来$A_0+A_2w_4^i+A_4(w_4^i)^2+A_6(w_4^i)^3$和$A_1+A_3w_4^i+A_5(w_4^i)^2+A_7(w_4^i)^3$的值,就可以算出$f(w_8^i)$和$f(w_8^{i+4})$的值

同理,算出$A_0+A_4(w_4^i)^2$和$A_2+A_6w_2^i$的值,不仅可以得到$A_0+A_2w_4^i+A_4(w_4^i)^2+A_6(w_4^i)^3$的值,还可以得到$A_0+A_2w_4^{i+2}+A_4(w_4^{i+2})^2+A_6(w_4^{i+2})^3$

然后将每一个序列分成长度为2的小序列(下面是用1~8来编号的)

后面的是重点,对于每一个长度为2的小序列(设为(x,y)):

  第一个元素$x$重新赋值为$x+w_1^0*y$

  第二个元素$y$重新赋值为$x-w_1^0*y$

(这个$w_1^1$这样来的:$w_{当前序列一半长度l}^{一半长度l的第i个元素,i从0到l-1}$)

重新赋值完之后就可以合并了,每两个长度为2的相邻序列合并为一个长度为4的序列

对于每一个长度为4的序列(设为(a,b,c,d)):

  第一个元素$a$重新赋值为$a+w_2^0*c$

  第三个元素$c$重新赋值为$a-w_2^0*c$

  第二个元素$b$重新赋值为$b+w_2^1*d$

  第四个元素$d$重新赋值为$b-w_2^1*d$

重新赋值完之后就可以合并了,每两个长度为4的相邻序列合并为一个长度为8的序列(此时全部小序列已经合并为一个序列了)

对于整个序列(长度为8),按照上面的赋值方法重新赋值(可以参考代码),于是FFT变换就ok了

两个序列就变成了

  $A=\left\{A_0',A_1',A_2',A_3',A_4',A_5',A_6',A_7'\right\}$

  $B=\left\{B_0',B_1',B_2',B_3',B_4',B_5',B_6',B_7'\right\}$

将两个序列对应元素相乘,得到新序列

  $C=\left\{A_0'*B_0',A_1'*B_1',A_2'*B_2',A_3'*B_3',A_4'*B_4',A_5'*B_5',A_6'*B_6',A_7'*B_7'\right\}$

然后将C序列进行FFT逆变换,就是多项式相乘后的系数序列了。

ps:FFT逆变换就是把当前序列再来一次FFT变换,只不过在重新赋值环节的时候是乘上$w_{mid}^{-1}$,而不是$w_{mid}^i$,最后得到的序列虚部为0,将实部全部除以8即可(这里不懂参考代码或前面逆变换的讲解)


FFT迭代版本


FFT的序列更新规律

综上所述,FFT的序列更新是有规律的,先把序列分解为序列长度为2的小段每一段更新完毕后就合并小段,继续一组一组更新,再合并,最后合并成一个序列,然后再最后一次更新完毕。

如图所示:

最后序列$\left\{A_0''',A_1''',...,A_{n-1}'''\right\}$为原序列进过FFT变换后的序列。

不像递归版本,迭代版本是直接从每一个小区间开始更新,然后合并,然后更新...,但是我们需要找到原系数序列经过奇偶分离的一系列操作后每个值的位置发生了什么变化。

假设原系数序列为$\left\{A_0,A_1,A_2,A_3,A_4,A_5,A_6,A_7\right\}$,下图是奇偶分离的示意图

没错,这不是巧合,序列经过奇偶分离之后,前后位置数的二进制是对称的,根据这一点我们就不需要用递归来实现,可以直接来得到奇偶分离最终序列,然后再进行FFT

FFT代码

FFT迭代版比递归版快的不是一点。。

关于序列奇偶分离可以用下面方法

int rev[(n+2)>>1];
for(i=;i<n;i++){
rev[i]=(rev[i>>]>>)|((i&)<<(bit-));
if (i<rev[i])swap(a[i],a[rev[i]]);
}

于是就可以实现迭代版本FFT

void FFT(Complex *a,int n,int inv){
int bit=,i,rev[(n+)>>];
while((<<bit)<n) bit++;
//交换序列
for(i=;i<n;i++){
rev[i]=(rev[i>>]>>)|((i&)<<(bit-));
if (i<rev[i])swap(a[i],a[rev[i]]);//if来保证只存在小的和大的交换,不会发生大的和小的交换
}
for (int mid=;mid<n;mid*=){//mid枚举区间长度的一半
Complex com(cos(pi/mid),inv*sin(pi/mid));//单位根
for (int i=;i<n;i+=mid*){//i来枚举每个区间的开头位置
Complex omega(,);
for (int j=;j<mid;j++,omega*=com){//j枚举每个区间的前一半元素
Complex x=a[i+j],y=omega*a[i+j+mid];
a[i+j]=x+y,a[i+j+mid]=x-y;//蝴蝶效应
}
}
}
}

FFT变换(迭代版)


总接代码


FFT递归版本

void FFT(Complex *a,int n,int inv){
//a是你要进行变换的数组(多项式系数数组),n是数组大小,inv=1表示正变换,inv=-1表示逆变换
if(n==) return;//如果分治后的序列大小为1,就直接返回,不用继续分治了
int mid=n>>;//如果n不等于1,那就继续分治,mid是分治后变成两个序列的长度
static Complex b[];//创建一个辅助用的b数组,后面体现用处
for(int i=;i<mid;i++) b[i]=a[*i],b[i+mid]=a[*i+];//将a数组奇数位置的值和偶数位置的值分开,b数组前mid个位置存a数组偶数位置值,后mid个位置存a数组奇数位置值
for(int i=;i<n;i++) a[i]=b[i];//将b数组值重新赋给a数组 //对分治后的两个序列(一个是a数组前mid个元素组成的序列,另一个数a数组后mid元素组成的序列) 进行FFT变换
FFT(a,mid,inv);
FFT(a+mid,mid,inv); //算出一半,得到另一半的值,a数组前mid元素相当于之前说的f1,a数组后mid元素相当于之前说的f2
for(int i=;i<mid;i++){
Complex com(cos(*pi*i/n),inv*sin(*pi*i/n));//
b[i]=a[i]+com*a[i+mid];b[i+mid]=a[i]-com*a[i+mid];
// cout<<n<<" "<<i<<" "<<b[i].real()<<" "<<b[i].imag()<<endl;
} for(int i=;i<n;i++) a[i]=b[i];//重新将b数组赋值给a数组
return;
}

FFT变换(迭代版)

FFT迭代版本

void FFT(Complex *a,int n,int inv){
//a是你要进行变换的数组(多项式系数数组),n是数组大小,inv=1表示正变换,inv=-1表示逆变换
int bit=,i,rev[(n+)>>];
while((<<bit)<n) bit++;
//交换序列
for(i=;i<n;i++){
rev[i]=(rev[i>>]>>)|((i&)<<(bit-));
if (i<rev[i])swap(a[i],a[rev[i]]);//if来保证只存在小的和大的交换,不会发生大的和小的交换
}
for (int mid=;mid<n;mid*=){//mid枚举区间长度的一半
Complex com(cos(pi/mid),inv*sin(pi/mid));//单位根
for (int i=;i<n;i+=mid*){//i来枚举每个区间的开头位置
Complex omega(,);
for (int j=;j<mid;j++,omega*=com){//j枚举每个区间的前一半元素
Complex x=a[i+j],y=omega*a[i+j+mid];
a[i+j]=x+y,a[i+j+mid]=x-y;//蝴蝶效应
}
}
}
}

FFT变换(迭代版)


学习:多项式算法----FFT的更多相关文章

  1. 多项式乘法(FFT)学习笔记

    ------------------------------------------本文只探讨多项式乘法(FFT)在信息学中的应用如有错误或不明欢迎指出或提问,在此不胜感激 多项式 1.系数表示法  ...

  2. 「学习笔记」FFT 之优化——NTT

    目录 「学习笔记」FFT 之优化--NTT 前言 引入 快速数论变换--NTT 一些引申问题及解决方法 三模数 NTT 拆系数 FFT (MTT) 「学习笔记」FFT 之优化--NTT 前言 \(NT ...

  3. 「学习笔记」FFT 快速傅里叶变换

    目录 「学习笔记」FFT 快速傅里叶变换 啥是 FFT 呀?它可以干什么? 必备芝士 点值表示 复数 傅立叶正变换 傅里叶逆变换 FFT 的代码实现 还会有的 NTT 和三模数 NTT... 「学习笔 ...

  4. 深度学习算子优化-FFT

    作者:严健文 | 旷视 MegEngine 架构师 背景 在数字信号和数字图像领域, 对频域的研究是一个重要分支. 我们日常"加工"的图像都是像素级,被称为是图像的空域数据.空域数 ...

  5. [置顶] 小白学习KM算法详细总结--附上模板题hdu2255

    KM算法是基于匈牙利算法求最大或最小权值的完备匹配 关于KM不知道看了多久,每次都不能完全理解,今天花了很久的时间做个总结,归纳以及结合别人的总结给出自己的理解,希望自己以后来看能一目了然,也希望对刚 ...

  6. 学习cordic算法所得(流水线结构、Verilog标准)

    最近学习cordic算法,并利用FPGA实现,在整个学习过程中,对cordic算法原理.FPGA中流水线设计.Verilog标准有了更加深刻的理解. 首先,cordic算法的基本思想是通过一系列固定的 ...

  7. 学习排序算法(一):单文档方法 Pointwise

    学习排序算法(一):单文档方法 Pointwise 1. 基本思想 这样的方法主要是将搜索结果的文档变为特征向量,然后将排序问题转化成了机器学习中的常规的分类问题,并且是个多类分类问题. 2. 方法流 ...

  8. 从 SGD 到 Adam —— 深度学习优化算法概览(一) 重点

    https://zhuanlan.zhihu.com/p/32626442 骆梁宸 paper插画师:poster设计师:oral slides制作人 445 人赞同了该文章 楔子 前些日在写计算数学 ...

  9. 吴裕雄 python 机器学习——集成学习AdaBoost算法回归模型

    import numpy as np import matplotlib.pyplot as plt from sklearn import datasets,ensemble from sklear ...

随机推荐

  1. 中标麒麟系统安装rpm文件

    打开终端,获得su权限. cd到rpm所在文件夹,输入指令,rpm -ivh rpm的名称

  2. HDU-3665 Seaside

    XiaoY is living in a big city, there are N towns in it and some towns near the sea. All these towns ...

  3. linux清理 clientmqueue 垃圾文件防止 inode 被占满

    #find /var/spool/clientmqueue/ -type -f |xargs rm -f

  4. 330-基于FMC接口的Kintex-7 XC7K325T PCIeX8 3U PXIe接口卡 光纤PCIe卡

    一.板卡概述      本板卡基于Xilinx公司的FPGAXC7K325T-2FFG900 芯片,pin_to_pin兼容FPGAXC7K410T-2FFG900 ,支持PCIeX8.64bit D ...

  5. docker安装各种坑

    今天记录一下之前安装docker遇到的各种坑. 我们从http://mirrors.aliyun.com/docker-toolbox/windows/docker-toolbox/这个网站下载. 下 ...

  6. openGL图形渲染管线

    在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应屏幕的2D像素.3D坐标转为2D坐标的处理过程是由OpenGL的图形渲 ...

  7. Java 多态概念、使用

    1.概念 2.多态的格式与使用 package Java12; /* 代码当中体现多态性,其实就是一句话: 父类引用指向子类对象 格式: 父类名称 对象名 = new 子类名称(): 或者: 接口名称 ...

  8. tpcc-mysql测试mysql5.6 (xfs文件系统)

    操作系统版本:CentOS release 6.5 (Final)  2.6.32-431.el6.x86_64 #1 内存:32G CPU:Intel(R) Xeon(R) CPU E5-2450 ...

  9. loadrunner 使用

    loadrunner给我的感觉很强势吧,第一次接触被安装包吓到了,当时用的是win10安装11版本的,各种安装失败,印象很深刻,那时候全班二三十号人,搞环境搞了两天,后来无奈,重做系统换成win7的了 ...

  10. centos启动提示unexpected inconsistency RUN fsck MANUALLY

    今天一台虚拟机背后的物理机故障了,主机迁移后变成了 read only filesystem.上面部署了很多长连接服务,没有关掉就直接reboot,报错: unexpected inconsisten ...