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. dp(买票优惠)

    CodeForces - 1154F There are n shovels in the nearby shop. The i-th shovel costs ai bourles. Misha h ...

  2. RMQ 的入门 hdu1806

    RMQ(Range Minimum/Maximum Query),即区间最值查询,是指这样一个问题:对于长度为n的数列A,回答若干次询问RMQ(i,j),返回数列A中下标在区间[i,j]中的最小/大值 ...

  3. Feign负载均衡

    Feign是一个声明式的Web Service客户端,比Ribbon好用,默认也是轮巡.我们只需要使用Feign创建一个接口,并用注解就好了.如果你基于spring cloud发布一个接口,实际上就是 ...

  4. Log4Net 之走进Log4Net (四)

    原文:Log4Net 之走进Log4Net (四) 一.Log4net的结构 log4net 有四种主要的组件,分别是Logger(记录器), Repository(库), Appender(附着器) ...

  5. C# Xml.Serialization 节点重命名

    XmlElement 节点重命名 XmlRoot 根节点重名称 XmlArray List集合添加根节点 XmlArrayItem List集合中子节点重命名 [Serializable] 将该类标记 ...

  6. C语言如何使输出的数字对齐

    右对齐%numd(num是位数,比如按5位数的长度输出,num为正数则右对齐) #include <stdio.h> int main() { printf(, ); printf(, ) ...

  7. Simple Vedio Intercom System

    I. Deployment  / Architecture Block Diagram II. Resources Used sip proxy server + sip user agent 1.  ...

  8. Installation of the latest version of netease-cloud-music on Fedora 30 linux platform

    Installation of the latest version of netease-cloud-music on Fedora 30 linux platform Abtract As we  ...

  9. Linux性能优化从入门到实战:03 CPU篇:CPU上下文切换

      linux操作系统是将CPU轮流分配给任务,分时执行的.而每次执行任务时,CPU需要知道CPU寄存器(CPU内置的内存)和程序计数器PC(CPU正在执行指令和下一条指令的位置)值,这些值是CPU执 ...

  10. [Tyvj1423]GF和猫咪的玩具(最短路)

    [Tyvj1423]GF和猫咪的玩具 题目描述 GF同学和猫咪得到了一个特别的玩具,这个玩具由n个金属环(编号为1---n),和m条绳索组成,每条绳索连接两个不同的金属环,并且长度相同.GF左手拿起金 ...