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

多项式

nn 次多项式可以形式化的写为:

f(x)=i=0naixif(x)=\sum\limits_{i=0}^{n}a_ix^i

其中序列 aa 可以叫做这个多项式的系数序列

在下文,我们会fif_i 表示多项式 ff 的系数序列 aa 的第 ii 项,即为 aia_i

多项式的表示方法

  • 系数表示法

显然,只要我们得知了系数序列,就可以唯一确定一个多项式。

  • 点值表示法

对于一个 nn 次多项式,代入 n+1n+1互不相同的值 xix_i,可以得到 n+1n+1 个满足 yi=f(xi)y_i=f(x_i)yiy_i。这时,序列 xx 和序列 yy 可以唯一确定 ff

因为有了 n+1n+1 个点 (xi,yi)(x_i,y_i),就可以通过插值来确定 ff

多项式的运算

f,gf,g 是两个 nn 次多项式,有:

(f+g)(x)=i=0n(fi+gi)xi(f+g)(x)=\sum\limits_{i=0}^n(f_i+g_i)x^i

(fg)(x)=k=02nxkin,jn,i+j=kfigj(f*g)(x)=\sum\limits_{k=0}^{2n}x^k\sum\limits_{i\le n,j\le n,i+j=k}f_ig_j

多项式除法可以使用多项式求逆做。

容易发现,在系数表示法下:

  • 多项式加法是 O(n)O(n) 的,因为直接系数相加就行

  • 多项式乘法是 O(n2)O(n^2) 的,因为需要枚举 i,ji,j

而在点值表示法下:(注意此时的运算需要满足序列 xx 相同)

  • 多项式加法是 O(n)O(n) 的,因为直接相加就行(乘法分配律)

  • 多项式乘法是 O(n)O(n) 的,因为直接让 yy 相乘就行

所以在点值表示法下做乘法运算是很优的!

但是直接通过代入把系数表示法转换为点值表示法是 O(n2)O(n^2) 的,而 FFT 就是把这一过程优化到了 O(nlogn)O(n\log n)

nn 次本原单位根

FFT 能优化时间复杂度的一个重要原因是它代入的不是随随便便的 n+1n+1 个不同的值,而是 nn 次本原单位根的 00 次方、11 次方一直到 nn 次方

nn 次单位根其实就是任意一个就是满足 xn=1x^n=1xx,这个东西在实数范围显然只有不多于两个,但是在虚数范围就有 nn 个了。

nn 次本原单位根则是一个特殊的 nn 次单位根 ωn\omega_n,满足 ωn0=ωn1=ωn2==ωnn\omega_n^0\not=\omega_n^1\not=\omega_n^2\not=\dots\not=\omega_n^n

想要构造一个 nn 次本原单位根,最好的方法就是把复平面上的单位圆平分成 nn。例如六次本原单位根 ω6\omega_6

显然,nn 次本原单位根 ωn=cos(2πn)+sin(2πn)i\omega_n=\operatorname{cos}(\dfrac{2\pi}{n})+\operatorname{sin}(\dfrac{2\pi}{n})i

我们考虑 ωn2\omega_n^2 是什么。因为虚数的乘法法则是模相乘,辐角相加,而 ωn\omega_n 的模是 11,所以 ωn2\omega_n^2 相当于 ωn\omega^n 转了一下

所以 ωnk=ωnkmodn\omega_n^k=\omega_n^{k\mod n},相当于转一圈后会转回来。本原单位根会转是一个很重要的性质!(虽然暂时没有用 )

DFT 和 IDFT

现在我们可以把单位根们代入多项式求值了,这就是 DFT。设 ff 是一个 n1n-1 次多项式,那么有:

f^k=i=0n1fiωnki\hat f_k=\sum\limits_{i=0}^{n-1}f_i\cdot\omega_n^{ki}

下面让我们证明 DFT 的逆变换 IDFT:

fk=1ni=0n1f^iωnkif_k=\dfrac{1}{n}\sum\limits_{i=0}^{n-1}\hat f_i\cdot\omega_n^{-ki}


将 DFT 的柿子代入:

fk=1ni=0n1ωnkij=0n1fjωnijf_k=\dfrac{1}{n}\sum\limits_{i=0}^{n-1}\omega_n^{-ki}\sum\limits_{j=0}^{n-1}f_j\cdot\omega_n^{ij}

=1nj=0n1fji=0n1ωnkiωnij=\dfrac{1}{n}\sum\limits_{j=0}^{n-1}f_j\sum\limits_{i=0}^{n-1}\omega_n^{-ki}\cdot\omega_n^{ij}

=1nj=0n1fji=0n1ωni(jk)=\dfrac{1}{n}\sum\limits_{j=0}^{n-1}f_j\sum\limits_{i=0}^{n-1}\omega_n^{i(j-k)}

考虑 i=0n1ωni(jk)\sum\limits_{i=0}^{n-1}\omega_n^{i(j-k)} 这部分:

  • j=kj=k,那么 i=0n1ωni(jk)=i=0n1ωn0=n\sum\limits_{i=0}^{n-1}\omega_n^{i(j-k)}=\sum\limits_{i=0}^{n-1}\omega_n^{0}=n

  • j=kj\not=k,那么因为 0j,k<n0\le j,k<n,所以 jk<n,ωnjk=0|j-k|<n,\omega_n^{j-k}\not=0,由等比数列求和公式得到:

i=0n1ωni(jk)=i=0n1(ωnjk)i\sum\limits_{i=0}^{n-1}\omega_n^{i(j-k)}=\sum\limits_{i=0}^{n-1}(\omega_n^{j-k})^i

=1(ωnjk)n1ωnjk=\dfrac{1-(\omega_n^{j-k})^n}{1-\omega_n^{j-k}}

=1(ωnn)jk1ωnjk=\dfrac{1-(\omega_n^n)^{j-k}}{1-\omega_n^{j-k}}

=111ωnjk=\dfrac{1-1}{1-\omega_n^{j-k}}

=0=0

所以

fk=1nj=0n1fjn[j=k]f_k=\dfrac{1}{n}\sum\limits_{j=0}^{n-1}f_j\cdot n\cdot[j=k]

=1nnfk=\dfrac{1}{n}\cdot n\cdot f_k

=fk=f_k

得证。


可以发现 DFT 和 IDFT 的柿子惊人地相似,只不过是多了个 1n\dfrac{1}{n} 和本原单位根指数上面的负号而已。所以我们只需要解决 DFT,IDFT 就能迎刃而解

FFT

ff 是一个次数为 n1n-1nn 是偶数的多项式(如果原来的 nn 是奇数那么可以补一项系数为 00 的项),设 m=n2m=\dfrac{n}{2},那么有:

f(x)=i=0n1fixif(x)=\sum\limits_{i=0}^{n-1}f_ix^i

根据奇偶性分类,有:

f(x)=i=0m1f2ix2i+i=0m1f2i+1x2i+1f(x)=\sum\limits_{i=0}^{m-1}f_{2i}x^{2i}+\sum\limits_{i=0}^{m-1}f_{2i+1}x^{2i+1}

=i=0m1f2ix2i+xi=0m1f2i+1x2i=\sum\limits_{i=0}^{m-1}f_{2i}x^{2i}+x\sum\limits_{i=0}^{m-1}f_{2i+1}x^{2i}

定义 f0(x)=i=0m1f2ixif0(x)=\sum\limits_{i=0}^{m-1}f_{2i}x^{i}f1(x)=i=0m1f2i+1xif1(x)=\sum\limits_{i=0}^{m-1}f_{2i+1}x^{i},那么有:

f(x)=f0(x2)+xf1(x2)f(x)=f0(x^2)+x \cdot f1(x^2)

在代入单位根之前,我们先来看两个单位根的性质:

ω2n2k=ωnk\omega_{2n}^{2k}=\omega_{n}^k

ω2nn+k=ω2nk\omega_{2n}^{n+k}=-\omega_{2n}^k

第一条性质是因为ω2n\omega_{2n} 的辐角转 2k2k 次相当于以 ωn\omega_{n} 的辐角转 kk

而第二条性质是因为ω2n\omega_{2n} 的辐角转 nn 次就到达了 1-1 的位置

记住这两条性质,代入单位根,对于满足 0k<m0\le k<mkk,有:

f(ωnk)=f0(ωn2k)+ωnkf1(ωn2k)f(\omega_n^k)=f0(\omega_n^{2k})+\omega_n^k\cdot f1(\omega_n^{2k})

=f0(ωmk)+ωnkf1(ωmk)=f0(\omega_m^k)+\omega_n^k\cdot f1(\omega_m^k)

f(ωnm+k)=f0(ωn2(m+k))+ωnm+kf1(ωn2(m+k))f(\omega_n^{m+k})=f0(\omega_n^{2(m+k)})+\omega_n^{m+k}\cdot f1(\omega_n^{2(m+k)})

=f0(ωmk)ωnkf1(ωmk)=f0(\omega_m^k)-\omega_n^{k}\cdot f1(\omega_m^k)

以上两个柿子十分相似,所以被称作蝴蝶操作

由蝴蝶操作不难得出,只要我们算出了 f0(ωm0,ωm1,ωm2,,ωmm1)f0(\omega_m^0,\omega_m^1,\omega_m^2,\dots,\omega_m^{m-1})f1(ωm0,ωm1,ωm2,,ωmm1)f1(\omega_m^0,\omega_m^1,\omega_m^2,\dots,\omega_m^{m-1}),就可以快速得到 f(ωn0,ωn1,ωn2,,ωnn1)f(\omega_n^0,\omega_n^1,\omega_n^2,\dots,\omega_n^{n-1})

f0(ωm0,ωm1,ωm2,,ωmm1)f0(\omega_m^0,\omega_m^1,\omega_m^2,\dots,\omega_m^{m-1})f1(ωm0,ωm1,ωm2,,ωmm1)f1(\omega_m^0,\omega_m^1,\omega_m^2,\dots,\omega_m^{m-1}) 又可以递归下去算,所以我们可以分治下去,用 O(nlogn)O(n\log n) 的时间复杂度完成 DFT

不过需要注意的是,nn 必须是 22 的幂,要不然无法分治

快速进行 IDFT 也相当好办,只要把 FFT 中用到的所有 ωn\omega_n 换成 ωn1\omega_n^{-1} ,最后再把所有点值乘上 1n\dfrac{1}{n} 即可

但是这还不够快,所以会被卡常。观察到影响效率的是按奇偶性分治,所以可以考虑令 rev(x)\operatorname{rev}(x) 表示 xx 的二进制反过来组成的数,fi=frev(i)f^\prime_i=f_{\operatorname{rev}(i)},那么在 ff^\prime 上的分治操作其实就是相邻两个序列的分治,这样可以大大加快 FFT 的速度,并且可以把 FFT 写成非递归的

代码如下:(模板题

#include <iostream>
#include <cstdio>
#include <cmath>

using namespace std;

const long long MS=5000005;
const double PI=acos(-1);

struct plex
{
    double x,y;
    plex(double a=0,double b=0) {x=a,y=b;}
};
plex operator+(plex a,plex b) {return plex(a.x+b.x,a.y+b.y);}
plex operator-(plex a,plex b) {return plex(a.x-b.x,a.y-b.y);}
plex operator*(plex a,plex b) {return plex(a.x*b.x-a.y*b.y,a.x*b.y+b.x*a.y);}

inline int getlen(int n)
{
    int res=1;
    while(res<n) res<<=1;
    return res;
}
int p_rev[MS],p_rev_lstn;
inline void FFT(int n,plex a[],int tpe)
{
    if(p_rev_lstn!=n)
    {
        p_rev_lstn=n;
        for(int i=0;i<n;i++) p_rev[i]=(p_rev[i>>1]>>1)|((i&1)?n>>1:0);
    }
    for(int i=0;i<n;i++) if(p_rev[i]<i) swap(a[p_rev[i]],a[i]);
    for(int mid=1;mid<n;mid<<=1)
    {
        int len=mid<<1;
        plex Wn=plex(cos(2*PI/len),tpe*sin(2*PI/len));
        for(int l=0;l<n-len+1;l+=len)
        {
            plex Wk=plex(1,0);
            for(int k=0;k<mid;k++,Wk=Wk*Wn)
            {
                plex x=a[l+k],y=Wk*a[l+mid+k];
                a[l+k]=x+y,a[l+mid+k]=x-y;
            }
        }
    }
}
inline void DFT(int n,plex a[]) {FFT(n,a,1);}
inline void IDFT(int n,plex a[])
{
    FFT(n,a,-1);
    for(int i=0;i<n;i++) a[i].x/=n,a[i].y/=n;
}

int n,m;
plex a[MS],b[MS];

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);
	}
	int len=getlen(n+m+1);
	DFT(len,a);
	DFT(len,b);
	for(int i=0;i<len;i++)
	{
		a[i]=a[i]*b[i];
	}
	IDFT(len,a);
	for(int i=0;i<n+m+1;i++)
	{
		printf("%d ",(int)(a[i].x+0.5));
	}
	printf("\n");
	return 0;
}