【数论数学】快速傅里叶变换(FFT)

3,063 阅读4分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

快速傅里叶变换 (Fast Fourier Transform) ,即利用计算机计算离散傅里叶变换(DFT)的快速计算方法,简称 FFT,于1965年由 J.W.库利和 T.W.图基提出。我们在这里只讨论其在算法竞赛中的运用。

问题定义

对于两个多项式 f(x)=i=0n1ai×xif(x)=\sum_{i=0}^{n-1}a^i\times x^ig(x)=i=0m1bi×xig(x)=\sum_{i=0}^{m-1}b^i\times x^i,求他们相乘后形成的多项式 f(x)×g(x)f(x)\times g(x)

问题分析

f(x)×g(x)=(f×g)(x)=(i=0n1ai×xi)(i=0m1bi×xi)=i=0n+m2ci×xi\begin{aligned} f(x)\times g(x)&=(f\times g)(x)\\ &=(\sum_{i=0}^{n-1}a^i\times x^i)(\sum_{i=0}^{m-1}b^i\times x^i)\\ &=\sum_{i=0}^{n+m-2}c_i\times x^i \end{aligned}

显然我们可以以 O(n2)O(n^2) 的复杂度暴力计算上式的每一项的系数。

for (int i=0;i<=n;++i)
    	for (int j=0;j<=m;++j) c[i+j]=a[i]*b[j];

考虑优化。

刚才我们定义了 f(x)=i=0n1ai×xif(x)=\sum_{i=0}^{n-1}a^i\times x^i,我们将其的 nn 个系数顺序罗列下来,就可以唯一确定这个多项式了。这种表示方法称为多项式的系数表示形式,如 f(x)f(x) 可以表示为

(a0,a1,a2,...,an1)(a_0,a_1,a_2,...,a_{n-1})

通过线性代数的相关知识我们知道,如果我们有该多项式所描述的线上的 n+1n+1 个点,也可以唯一确定这个多项式,这种表示方法称为多项式的点值表示形式。所以 f(x)f(x) 也可以记为

{(x0,f(x0)),(x1,f(x1)),...,(xn1,f(xn1))}\{(x_0,f(x_0)),(x_1,f(x_1)),...,(x_{n-1},f(x_{n-1}))\}

如果我们分别在 f(x)f(x)g(x)g(x) 上确定 n+m1n+m-1 个点

{(x0,f(x0)),(x1,f(x1)),...,(xn+m2,f(xn+m2))}{(x0,g(x0)),(x1,g(x1)),...,(xn+m2,g(xn+m2))}\{(x_0,f(x_0)),(x_1,f(x_1)),...,(x_{n+m-2},f(x_{n+m-2}))\}\\ \{(x_0,g(x_0)),(x_1,g(x_1)),...,(x_{n+m-2},g(x_{n+m-2}))\}

f(x)×g(x)f(x)\times g(x) 的点值表示形式如下

{(x0,f(x0)×g(x0)),(x1,f(x1)×g(x1)),...,(xn+m2,f(xn+m2)×g(xn+m2))}\{(x_0,f(x_0)\times g(x_0)),(x_1,f(x_1)\times g(x_1)),...,(x_{n+m-2},f(x_{n+m-2})\times g(x_{n+m-2}))\}

如果我们可以做到快速转换多项式的系数表示和点值表示,我们就可以用 O(n)O(n) 的时间复杂度计算 f(x)×g(x)f(x)\times g(x) 的结果了!但是如果我们给多项式带入 n+m1n+m-1 个点去求点值表示,变量的每个取值都需要 O(n)O(n) 的计算对应的函数值,总的时间复杂度是 O(n2)O(n^2) 级别,甚至比暴力做常数大得多(

而 FFT 可以实现在 O(nlogn)O(nlogn) 级别实现多项式点值表示与系数表示的相互转化。

DFT

DFT 的任务是把一个多项式从系数表示转化为点值表示。

我们考虑将多项式从系数表示转化为点值表示的时间瓶颈是什么。显然我们必须要最终得到 nn 个点,而每个自变量去求对应函数值时又不得不进行 O(n)O(n) 级别的运算,所以我们希望对不同自变量求解中产生的过程量可以互相利用。加之即使自变量的值为 2,在指数级运算中数值增大的速度也难以接受。容易发现 11 是个很不错的选择,为了凑够 nn 个点,我们把视野转向了复平面。

单位根

相信看到这里的大家都知道什么是复平面吧_(:3

在复平面中的单位圆上的点模长均为 11,如下图所示

image.png

则单位圆上所有的点可以表示为 (cos(θ),i×sin(θ))(cos(\theta),i\times sin(\theta))

nn 次单位根是指 n 次幂为 1 的复数。

由欧拉公式

ei×x=cos(x)+i×sin(x)e^{i\times x}=cos(x)+i\times sin(x)

x=πx=\pi,且 n,kN+n,k\in N+

eiπ=1e2iπ=1e2kiπ=1e2kiπn=1ne2kiπn=1n\begin{aligned} e^{i\pi}&=-1\\ e^{2i\pi}&=1\\ e^{2ki\pi}&=1\\ \sqrt[n]{e^{2ki\pi}}&=\sqrt[n]{1}\\ e^\frac{2ki\pi}{n}&=\sqrt[n]{1}\\ \end{aligned}

所以 nn 次单位根恰有 nn 个且第 kk 个是

e2kiπn=cos(2kπn)+i×sin(2kπn)e^\frac{2ki\pi}{n}=cos(\frac{2k\pi}{n})+i\times sin(\frac{2k\pi}{n})

由上式我们可以发现,单位根实际上就是把 (1,0)(1,0) 作为其中一个等分点,把复平面上的单位圆进行 nn 等分,然后从原点指向 nn 个等分点形成的向量。其中幅角为正且最小的是 e2iπn=cos(2πn)+i×sin(2πn)e^\frac{2i\pi}{n}=cos(\frac{2\pi}{n})+i\times sin(\frac{2\pi}{n}),我们把它记作 ωn\omega_n

由三角函数的性质和基本数学运算法则可以得到单位根具有以下性质:

  1. 共有 nn 个不同的 nn 次单位根,其中第 kk 个就是 ωnk\omega_n^k
  2. 对所有的整数 pp,都满足 ωnk=ωpnpk\omega_n^k=\omega_{pn}^{pk}
  3. ωnk+n2=ωnk\omega_n^{k+\frac{n}{2}}=-\omega_n^{k}
  4. ωnk+n=ωnk\omega_n^{k+n}=\omega_n^{k}
  5. i=0n1ωni=0\sum_{i=0}^{n-1}\omega_n^i=0

前四条都比较显然,我们证明一下第五条性质:

首先令

S=1+x+x2+x3+...+xn1S=1+x+x^2+x^3+...+x^{n-1}

xS=x+x2+x3+...+xn1+xnxS=x+x^2+x^3+...+x^{n-1}+x^n

两式作差可以得到

xSS=(x+x2+x3+...+xn1+xn)(1+x+x2+x3+...+xn1)=xn1=(x1)×(1+x+x2+x3+...+xn1)\begin{aligned} xS-S&=(x+x^2+x^3+...+x^{n-1}+x^n)-(1+x+x^2+x^3+...+x^{n-1} )\\ &=x^n-1\\ &=(x-1)\times(1+x+x^2+x^3+...+x^{n-1}) \end{aligned}

xn1=(x1)×(1+x+x2+x3+...+xn1)x^n-1=(x-1)\times(1+x+x^2+x^3+...+x^{n-1}) 可以得到

  • x=1x=1 时,S=n。
  • 否则,若 xn=1x^n=1,则 S=0。

得证 i=0n1ωni=0\sum_{i=0}^{n-1}\omega_n^i=0

那么我们把单位根作为自变量有什么用处呢?

FFT

对于一个多项式 f(x)=i=0n1ai×xif(x)=\sum_{i=0}^{n-1}a^i\times x^i,不失一般性的,我们可以把 nn 视为 2k2^k,即整个多项式一共有 2k2^k 项,高位补 0。我们先对其进行一定的转化

f(x)=i=0n1ai×xi=a0×x0+a1×x1+...+an×xn=(a0×x0+a2×x2+...+an2×xn2)+(a1×x1+a3×x3+...+an1×xn1)=(a0×x0+a2×x2+...+an2×xn2)+x×(a1×x0+a3×x2+...+an1×xn2)\begin{aligned} f(x)&=\sum_{i=0}^{n-1}a^i\times x^i\\ &=a_0\times x^0+a_1\times x^1+...+a_n\times x^n\\ &=(a_0\times x^0+a_2\times x^2+...+a_{n-2}\times x^{n-2})+(a_1\times x^1+a_3\times x^3+...+a_{n-1}\times x^{n-1})\\ &=(a_0\times x^0+a_2\times x^2+...+a_{n-2}\times x^{n-2})+x \times(a_1\times x^0+a_3\times x^2+...+a_{n-1}\times x^{n-2}) \end{aligned}

f1(x)=a0×x0+a2×x1+...+an2×xn21f2(x)=a1×x0+a3×x1+...+an1×xn21f_1(x)=a_0\times x^0+a_2\times x^1+...+a_{n-2}\times x^{\frac{n}{2}-1}\\ f_2(x)=a_1\times x^0+a_3\times x^1+...+a_{n-1}\times x^{\frac{n}{2}-1}

f(x)=f1(x2)+x×f2(x2)f(x)=f_1(x^2)+x\times f_2(x^2)

nn 次单位根带入后可以得到

f(ωnk)=f1(ωn2k)+ωnk×f2(ωn2k)=f1(ωn2k)+ωnk×f2(ωn2k)\begin{aligned} f(\omega_n^k)&=f_1(\omega_n^{2k})+\omega_n^{k}\times f_2(\omega_n^{2k})\\ &=f_1(\omega_{\frac{n}{2}}^{k})+\omega_n^{k}\times f_2(\omega_{\frac{n}{2}}^{k})\\ \end{aligned}

kn2k\ge\frac{n}{2} 时,令 k=kn2k'=k-\frac{n}{2},可以得到

f(ωnk)=f1(ωn22(k+n2))+ωnk+n2×f2(ωn22(k+n2))=f1(ωn2k)ωnk×f2(ωn2k) \begin{aligned} f(\omega_n^k)&=f_1(\omega_{\frac{n}{2}}^{2(k'+\frac{n}{2})})+\omega_n^{k'+\frac{n}{2}}\times f_2(\omega_{\frac{n}{2}}^{2(k'+\frac{n}{2})})\\ &=f_1(\omega_{\frac{n}{2}}^{k'})-\omega_n^{k'}\times f_2(\omega_{\frac{n}{2}}^{k'}) \end{aligned}

如果我们递归地做下去,我们只要知道两个多项式 f1(x)f_1(x)f2(x)f_2(x)(ωn20,ωn21,...,ωn2n21)(\omega_\frac{n}{2}^0,\omega_\frac{n}{2}^1,...,\omega_\frac{n}{2}^{\frac{n}{2}-1}) 处的函数值,就可以求出 f(x)f(x)(ωn0,ωn1,...,ωnn1)(\omega_n^0,\omega_n^1,...,\omega_n^{n-1}) 处的函数值了。当 n=1n=1 时,ω1=1\omega_1=1,则函数值就是多项式系数表示的系数。

总体复杂度是 O(nlogn)O(nlogn)

IDFT

接下来我们考虑怎么把多项式从点值表示转化回系数表示。

如果我们把 DFT 表示为矩阵形式,就可以得到

[(ωn0)0(ωn0)1(ωn0)2...(ωn0)n1(ωn1)0(ωn1)1(ωn1)2...(ωn1)n1(ωn2)0(ωn2)1(ωn2)2...(ωn2)n1(ωnn1)0(ωnn1)1(ωnn1)2...(ωnn1)n1][a0a1a2an1]=[y0y1y2yn1]\begin{bmatrix} (\omega_n^0)^0&(\omega_n^0)^1&(\omega_n^0)^2&...&(\omega_n^0)^{n-1}\\ (\omega_n^1)^0&(\omega_n^1)^1&(\omega_n^1)^2&...&(\omega_n^1)^{n-1}\\ (\omega_n^2)^0&(\omega_n^2)^1&(\omega_n^2)^2&...&(\omega_n^2)^{n-1}\\ \vdots&\vdots&\vdots&\ddots&\vdots\\ (\omega_n^{n-1})^0&(\omega_n^{n-1})^1&(\omega_n^{n-1})^2&...&(\omega_n^{n-1})^{n-1}\\ \end{bmatrix} \begin{bmatrix} a_0\\a_1\\a_2\\\vdots\\a_{n-1} \end{bmatrix} = \begin{bmatrix} y_0\\y_1\\y_2\\\vdots\\y_{n-1} \end{bmatrix}

其中 aia_i 是原多项式 ii 次方项的系数,yjy_j 是自变量 ωnj\omega_n^j 对应的函数值。 令

Ω=[(ωn0)0(ωn0)1(ωn0)2...(ωn0)n1(ωn1)0(ωn1)1(ωn1)2...(ωn1)n1(ωn2)0(ωn2)1(ωn2)2...(ωn2)n1(ωnn1)0(ωnn1)1(ωnn1)2...(ωnn1)n1]\Omega= \begin{bmatrix} (\omega_n^0)^0&(\omega_n^0)^1&(\omega_n^0)^2&...&(\omega_n^0)^{n-1}\\ (\omega_n^1)^0&(\omega_n^1)^1&(\omega_n^1)^2&...&(\omega_n^1)^{n-1}\\ (\omega_n^2)^0&(\omega_n^2)^1&(\omega_n^2)^2&...&(\omega_n^2)^{n-1}\\ \vdots&\vdots&\vdots&\ddots&\vdots\\ (\omega_n^{n-1})^0&(\omega_n^{n-1})^1&(\omega_n^{n-1})^2&...&(\omega_n^{n-1})^{n-1}\\ \end{bmatrix}

则我们只需要找到 Ω\Omega 的一个逆矩阵 Ω1\Omega^{-1},就可以将多项式由点值表示转化为系数表示了。怎样找 Ω\Omega 的逆矩阵呢?

我们刚才证明了 nn 次单位根的和为 00,受到这一特性的启发,我们发现

[(ωnni)0(ωnni)1(ωnni)2...(ωnni)n1][(ωnj)0(ωnj)1(ωnj)2(ωnj)n1]=k=0n1(ωnni)k(ωnj)k=k=0n1(ωnni×ωnj)k=k=0n1(ωnni+j)k={n   (i=j)0   (ij)\begin{aligned} \begin{bmatrix} (\omega_n^{n-i})^0&(\omega_n^{n-i})^1&(\omega_n^{n-i})^2&...&(\omega_n^{n-i})^{n-1} \end{bmatrix} \begin{bmatrix} (\omega_n^j)^0\\(\omega_n^j)^1\\(\omega_n^j)^2\\\vdots\\(\omega_n^j)^{n-1} \end{bmatrix} &= \sum_{k=0}^{n-1}(\omega_n^{n-i})^k(\omega_n^j)^k\\ &=\sum_{k=0}^{n-1}(\omega_n^{n-i}\times \omega_n^j)^k\\ &=\sum_{k=0}^{n-1}(\omega_n^{n-i+j})^k\\ &= \begin{cases} n\ \ \ (i=j)\\ 0\ \ \ (i\neq j) \end{cases} \end{aligned}

所以我们可以得到 Ω\Omega 的一个逆矩阵

Ω1=1n[(ωnn)0(ωnn)1(ωnn)2...(ωnn)n1(ωnn1)0(ωnn1)1(ωnn1)2...(ωnn1)n1(ωnn2)0(ωnn2)1(ωnn2)2...(ωnn2)n1(ωn1)0(ωn1)1(ωn1)2...(ωn1)n1]=1n[(ωn0)0(ωn0)1(ωn0)2...(ωn0)n1(ωn1)0(ωn1)1(ωn1)2...(ωn1)n1(ωn2)0(ωn2)1(ωn2)2...(ωn2)n1(ωnn1)0(ωnn1)1(ωnn1)2...(ωnn1)n1]\begin{aligned} \Omega^{-1}&= \frac{1}{n} \begin{bmatrix} (\omega_n^n)^0&(\omega_n^n)^1&(\omega_n^n)^2&...&(\omega_n^n)^{n-1}\\ (\omega_n^{n-1})^0&(\omega_n^{n-1})^1&(\omega_n^{n-1})^2&...&(\omega_n^{n-1})^{n-1}\\ (\omega_n^{n-2})^0&(\omega_n^{n-2})^1&(\omega_n^{n-2})^2&...&(\omega_n^{n-2})^{n-1}\\ \vdots&\vdots&\vdots&\ddots&\vdots\\ (\omega_n^1)^0&(\omega_n^1)^1&(\omega_n^1)^2&...&(\omega_n^1)^{n-1}\\ \end{bmatrix}\\ &= \frac{1}{n} \begin{bmatrix} (\overline{\omega_n^0})^0&(\overline{\omega_n^0})^1&(\overline{\omega_n^0})^2&...&(\overline{\omega_n^0})^{n-1}\\ (\overline{\omega_n^1})^0&(\overline{\omega_n^1})^1&(\overline{\omega_n^1})^2&...&(\overline{\omega_n^1})^{n-1}\\ (\overline{\omega_n^2})^0&(\overline{\omega_n^2})^1&(\overline{\omega_n^2})^2&...&(\overline{\omega_n^2})^{n-1}\\ \vdots&\vdots&\vdots&\ddots&\vdots\\ (\overline{\omega_n^{n-1}})^0&(\overline{\omega_n^{n-1}})^1&(\overline{\omega_n^{n-1}})^2&...&(\overline{\omega_n^{n-1}})^{n-1}\\ \end{bmatrix} \end{aligned}

所以将一个多项式在分治的过程中乘上的单位根变为其共轭复数,分治完的每一项除以 nn 即可得到原多项式的每一项系数。

递归实现

#include <iostream>
#include <cstdio>
#include <cmath>
#include <vector>
using namespace std;
const double Pi=acos(-1.0);
struct asdf {
    long double x,y;
    asdf operator + (const asdf b) const { return (asdf){x+b.x,y+b.y}; }
	asdf operator - (const asdf b) const { return (asdf){x-b.x,y-b.y}; }
	asdf operator * (const asdf b) const { return (asdf){x*b.x-y*b.y,x*b.y+y*b.x}; }
	asdf operator * (const int b) const {return (asdf){x,y*b}; }
};
int N,M,l,tot=1;
void FFT(int len,vector<asdf> &a,int qwq)
{
	if (len==1) return;
	vector<asdf> a1(len>>1),a2(len>>1);
	for (int i=0;i<len;++i)
		if (i%2) a2[i>>1]=a[i];
		else a1[i>>1]=a[i];
	FFT(len>>1,a1,qwq);
	FFT(len>>1,a2,qwq);
	asdf Wn={cos(2.0*Pi/len),qwq*sin(2.0*Pi/len)};
	asdf w={1,0};
	for (int i=0;i<(len>>1);i++)
	{
		a[i]=a1[i]+w*a2[i];
		a[i+(len>>1)]=a1[i]-w*a2[i];
		w=w*Wn;
	}
}
int main()
{
    int N,M;
    scanf("%d%d",&N,&M);
    for (;tot<=N+M;l++) tot<<=1;
    vector<asdf> a(tot+1),b(tot+1);
    for (int i=0;i<=N;i++) scanf("%Lf",&a[i].x);
    for (int i=0;i<=M;i++) scanf("%Lf",&b[i].x);
    FFT(tot,a,1),FFT(tot,b,1);
    for (int i=0;i<=tot;i++) a[i]=a[i]*b[i];
    FFT(tot,a,-1);
    for (int i=0;i<=N+M;i++)
    {
    	printf("%lld ",(long long)(a[i].x/tot+0.5));
    }
		
    return 0;
}

非递归实现

但是这样计算常数很大,我们试图用非递归方法实现。

考虑一下奇偶分开时发生了什么。比如一个长度为 88 的多项式的分治过程为

 (a0,a1,a2,a3,a4,a5,a6,a7)(a0,a2,a4,a6)(a1,a3,a5,a7)(a0,a4)(a2,a6)(a1,a5)(a3,a7)(a0)(a4)(a2)(a6)(a1)(a5)(a3)(a7)\ (a_{0}, a_{1}, a_{2}, a_{3}, a_{4}, a_{5}, a_{6}, 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})

如果我们可以直接将数组变成最后一步的样子,就可以自下而上递推计算了。

观察上述过程可以发现,第一次奇偶分列时,下标最低位是 00 的系数被我们分去了前半段。第二次整个序列长度减小了一半,可以视为将所有元素的下标整除 22 后再将下标最低位是 00 的系数分给前半段。以此类推。

所以系数 aia_i 最后到达的位置是 ii 的二进制位逆序后的结果。这个东西可以递推预处理。令 r[i]r[i] 表示变换后 ii 应该在的位置,多项式的长度为 tottot,即多项式系数的下标为 [0,tot1][0,tot-1],不失一般性将 tottot 视为 22 的幂且令 n=log2(tot)n=log_2(tot),分析如下:

假设我们当前枚举到了 i=j=0n1sj×2ji=\sum_{j=0}^{n-1}s_j\times 2^j,其二进制表示为

i=(sn1,sn2,...,s1,s0)2i=(s_{n-1},s_{n-2},...,s_1,s_0)_2

将其右移一位得到

i>>1=(0,sn1,sn2,...,s1)2i>>1=(0,s_{n-1},s_{n-2},...,s_1)_2

那么我们已经求得了将 i>>1i>>1 翻转后的结果

r[i>>1]=(s1,s2,...,sn2,sn1,0)2r[i>>1]=(s_1,s_2,...,s_{n-2},s_{n-1},0)_2

所以我们只需要将 r[i>>1]r[i>>1] 右移一位再或上 s0<<(n1)s_{0}<<(n-1),就可以得到

r[i]=(s0,s1,s2,...,sn2,sn1)2r[i]=(s_0,s_1,s_2,...,s_{n-2},s_{n-1})_2

综上,转移方程为:

r[i]=(r[i>>1]>>1)(((i&1)<<(log2(tot)1))r[i]=(r[i>>1]>>1)|(((i\&1)<<(\log_2(tot)-1))

下面给出非递归实现的代码。

#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
const double Pi=acos(-1.0);
struct asdf
{
    double x,y;
}a[10000010],b[10000010];
asdf operator + (asdf a,asdf b)
{
	return (asdf){a.x+b.x,a.y+b.y};
}
asdf operator - (asdf a,asdf b)
{ 
	return (asdf){a.x-b.x,a.y-b.y};
}
asdf operator * (asdf a,asdf b)
{ 
	return (asdf){a.x*b.x-a.y*b.y,a.x*b.y+a.y*b.x};
}
int N,M,l,r[10000010],tot=1;
void FFT(asdf *A,int qwq)
{
    for (int i=0;i<tot;i++) 
        if (i<r[i]) swap(A[i],A[r[i]]);
    for (int mid=1;mid<tot;mid<<=1)
	{
        asdf Wn=(asdf){cos(Pi/mid),qwq*sin(Pi/mid)};
        for (int R=mid<<1,j=0;j<tot;j+=R)
        {
            asdf w=(asdf){1,0};
            for (int k=0;k<mid;k++,w=w*Wn) 
            {
                asdf x=A[j+k],y=w*A[j+mid+k];
                A[j+k]=x+y,A[j+mid+k]=x-y;
            }
        }
    }
}
int main()
{
    int N,M;
    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);
    for (;tot<=N+M;l++) tot<<=1;
    for (int i=0;i<tot;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(l-1));
    FFT(a,1),FFT(b,1);
    for (int i=0;i<=tot;i++) a[i]=a[i]*b[i];
    FFT(a,-1);
    for (int i=0;i<=N+M;i++) printf("%d ",(int)(a[i].x/tot+0.5));
    return 0;
}

预处理优化精度

队友用我之前写的上面那个板子在代码源提交大 WA 特 WA,发现精度损失主要出现在我们求第 kknn 次单位根时使用了乘法。

如果我们将其改成每次使用单位根时用用公式直接计算,需要多次计算三角函数值,很容易 TLE。

我们可以直接预估程序需要处理的项数最多的多项式有多少项,并将其补成 22 的若干次幂,记为 FFTNFFTN。直接预处理好 FFTNFFTN 次单位根,每次使用时就可以通过性质

对所有的整数 pp,都满足 ωnk=ωpnpk\omega_n^k=\omega_{pn}^{pk}

快速且精度较高地求出我们需要的单位根。

代码如下:

#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
const double Pi=acos(-1.0);
const int FFTN=262144; 
struct asdf {
    long double x,y;
    asdf operator + (const asdf b) const { return (asdf){x+b.x,y+b.y}; }
	asdf operator - (const asdf b) const { return (asdf){x-b.x,y-b.y}; }
	asdf operator * (const asdf b) const { return (asdf){x*b.x-y*b.y,x*b.y+y*b.x}; }
	asdf operator * (const int b) const {return (asdf){x,y*b}; }
}a[10000010],b[10000010],nw[10000010];

int N,M,l,r[10000010],tot=1;
void FFT(asdf *A,int qwq)
{
    for (int i=0;i<tot;i++) 
        if (i<r[i]) swap(A[i],A[r[i]]);
    for (int mid=1;mid<tot;mid<<=1)
	{
        for (int R=mid<<1,j=0;j<tot;j+=R)
        {
        	int t=FFTN/2/mid;
            for (int k=0;k<mid;k++) 
            {
                asdf x=A[j+k],y=nw[t*k]*qwq*A[j+mid+k];
                A[j+k]=x+y,A[j+mid+k]=x-y;
            }
        }
    }
}
int main()
{
	for (int i=0;i<FFTN;++i)
		nw[i]=(asdf){cosl(2*Pi*i/FFTN),sinl(2*Pi*i/FFTN)};
    int N,M;
    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);
    for (;tot<=N+M;l++) tot<<=1;
    for (int i=0;i<tot;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(l-1));
    FFT(a,1),FFT(b,1);
    for (int i=0;i<=tot;i++) a[i]=a[i]*b[i];
    FFT(a,-1);
    for (int i=0;i<=N+M;i++) printf("%lld ",(long long)(a[i].x/tot+0.5));
    return 0;
}

其他

虽然上面的代码已经比较优秀了,但是我们还可以对常数进行进一步的优化,日后遇到卡常的题目我再来进行补充。