快速傅里叶变换FFT与快速数论变换NTT入门

一.FFT引入.

离散傅里叶变换DFT,是一种用于将多项式从系数表示转化为点值表示的多项式变换.

快速傅里叶变换FFT,是一种 O ( n log ⁡ n ) O(n\log n) O(nlogn)的傅里叶变换,在OI中占有重要地位,主要用与优化卷积过程.

卷积,即给定 a i , b i a_i,b_i ai,bi,求 c i = ∑ i = 0 n a i b n − i c_i=\sum_{i=0}^{n}a_ib_{n-i} ci=i=0naibni.

为什么要用FFT来优化卷积,因为一个卷积直接求的复杂度是 O ( n 2 ) O(n^2) O(n2)的…


二.系数表示与点值表示.

多项式主要有两种表示,一个是系数表示,另一个是点值表示.

系数表示:即、对于一个多项式 F ( x ) = ∑ i = 0 n a i x i F(x)=\sum_{i=0}^{n}a_ix^i F(x)=i=0naixi,用系数 a i a_i ai表示一个多项式被称为多项式的系数表示.

点值表示:对于一个多项式 F ( x ) = ∑ i = 0 n a i x i F(x)=\sum_{i=0}^{n}a_ix^i F(x)=i=0naixi,用 n + 1 n+1 n+1个点 ( x i , F ( x i ) ) (x_i,F(x^i)) (xi,F(xi))表示一个多项式被称为点值表示.

考虑在做卷积的时候,系数表示明显要用 O ( n 2 ) O(n^2) O(n2)的时间,而对于点值表示的两个多项式 A ( x ) , B ( x ) A(x),B(x) A(x),B(x),发现它们卷积起来的点值表示就是 ( x i , A ( x i ) B ( x i ) ) (x_i,A(x_i)B(x_i)) (xi,A(xi)B(xi)),也就是说时间复杂度为 O ( n ) O(n) O(n).

突然感觉卷积只需要 O ( n ) O(n) O(n)即可解决了.只是我们一般用的是系数表示,直接系数表示可是没法 O ( n ) O(n) O(n)卷积的,所以我们要在这两种形式之间转换.


三.点值与插值.

点值:多项式从系数表示点值表示.

插值:多项式从点值表示系数表示.

对于点值,我们可以随意带入 n + 1 n+1 n+1个横坐标 x 0 , x 1 , x 2 , . . . , x n x_0,x_1,x_2,...,x_n x0,x1,x2,...,xn,分别求得对应的 F ( x 0 ) , F ( x 1 ) , F ( x 2 ) , . . . , F ( x n ) F(x_0),F(x_1),F(x_2),...,F(x_n) F(x0),F(x1),F(x2),...,F(xn),时间复杂度 O ( n 2 ) O(n^2) O(n2).

这个东西的速度与直接系数表示做卷积同级了…

不管了考虑插值.我们得到 n + 1 n+1 n+1个点值 ( x 0 , F ( x 0 ) ) , ( x 1 , F ( x 1 ) ) , . . . , ( x 2 , F ( x n ) ) (x_0,F(x_0)),(x_1,F(x_1)),...,(x_2,F(x_n)) (x0,F(x0)),(x1,F(x1)),...,(x2,F(xn)),那么可以列出方程组:
{ x 0 0 a 0 + x 0 1 a 1 + ⋯ + x 0 n a n = F ( x 0 ) x 1 0 a 0 + x 1 1 a 1 + ⋯ + x 1 n a n = F ( x 1 ) ⋮ x n 0 a 0 + x n 1 a 1 + ⋯ + x n n a n = F ( x n ) \left\{\begin{matrix} x_0^0a_0+x_0^1a_1+\cdots+x_0^na_n=F(x_0)\\ x_1^0a_0+x_1^1a_1+\cdots+x_1^na_n=F(x_1)\\ \vdots\\ x_n^0a_0+x_n^1a_1+\cdots+x_n^na_n=F(x_n) \end{matrix}\right. x00a0+x01a1++x0nan=F(x0)x10a0+x11a1++x1nan=F(x1)xn0a0+xn1a1++xnnan=F(xn)

我们就可以列出一个矩阵方程:
[ 1 x 0 x 0 2 ⋯ x 0 n 1 x 1 x 1 2 ⋯ x 1 n ⋮ ⋮ ⋮ ⋱ ⋮ 1 x n x n 2 ⋯ x n n ] [ a 0 a 1 ⋮ a n ] = [ F ( x 0 ) F ( x 1 ) ⋮ F ( x n ) ] \left[\begin{matrix} 1&x_0&x_0^2&\cdots&x_0^n\\ 1&x_1&x_1^2&\cdots&x_1^n\\ \vdots&\vdots&\vdots&\ddots&\vdots\\ 1&x_n&x_n^2&\cdots&x_n^n \end{matrix}\right] \left[\begin{matrix} a_0\\ a_1\\ \vdots\\ a_n\\ \end{matrix}\right]= \left[\begin{matrix} F(x_0)\\ F(x_1)\\ \vdots\\ F(x_n)\\ \end{matrix}\right] 111x0x1xnx02x12xn2x0nx1nxnna0a1an=F(x0)F(x1)F(xn)

这里有一个特殊矩阵的出现,即范德蒙矩阵.

范德蒙矩阵:我们将等式左边 ( n + 1 ) ∗ ( n + 1 ) (n+1)*(n+1) (n+1)(n+1)的矩阵称为范德蒙矩阵,表示为 V ( x 0 , x 1 , . . . , x n ) V(x_0,x_1,...,x_n) V(x0,x1,...,xn),即:
V ( x 0 , x 1 , . . . , x n ) = [ 1 x 0 x 0 2 ⋯ x 0 n 1 x 1 x 1 2 ⋯ x 1 n ⋮ ⋮ ⋮ ⋱ ⋮ 1 x n x n 2 ⋯ x n n ] V(x_0,x_1,...,x_n)= \left[\begin{matrix} 1&x_0&x_0^2&\cdots&x_0^n\\ 1&x_1&x_1^2&\cdots&x_1^n\\ \vdots&\vdots&\vdots&\ddots&\vdots\\ 1&x_n&x_n^2&\cdots&x_n^n \end{matrix}\right] V(x0,x1,...,xn)=111x0x1xnx02x12xn2x0nx1nxnn

由于我们要求出的是 a i a_i ai,高斯消元就可以了,时间复杂度 O ( n 3 ) O(n^3) O(n3)

直接比系数表示求卷积还慢了…

不过插值还是可以 O ( n 2 ) O(n^2) O(n2)求的,利用拉格朗日插值法即可.

然后就可以直接 O ( n 2 ) O(n^2) O(n2)求插值了,然而还是与系数表示直接卷积同级,常数还大的一匹…

不过从上面我们可以看到的是,一次多项式卷积的过程可以分这几步走:
1.首先注意到 C ( x ) = A ( x ) B ( x ) C(x)=A(x)B(x) C(x)=A(x)B(x),若 A , B A,B A,B的最高次指数为 n n n,那么 C C C的最高次指数为 2 n 2n 2n,所以我们先把 A , B A,B A,B的最高次指数填到 2 n 2n 2n.
2.多项式点值.
3.点值表示下多项式乘法.
4.多项式插值.


四.一些在下面会用到的复数知识.

虚数单位:虚数单位 i i i满足 i 2 = − 1 i^2=-1 i2=1.

虚数:一个虚数可以用 a i ai ai来表示.

复数:一个复数可以表示为 z = a + b i z=a+bi z=a+bi,其中 a a a被称为实部, b b b被称为虚部, i i i为虚数单位.

复数集合符号为 C C C.

复数加减法:实部对应实部,虚部对应虚部.即:
z 1 = a 1 + b 1 i , z 2 = a 2 + b 2 i z 1 ± z 2 = ( a 1 + b 1 i ) ± ( a 2 + b 2 i ) = ( a 1 ± a 2 ) + ( b 1 ± b 2 ) i z_1=a_1+b_1i,z_2=a_2+b_2i\\ z_1\pm z_2=(a_1+b_1i)\pm(a_2+b_2i)=(a_1\pm a_2)+(b_1\pm b_2)i z1=a1+b1i,z2=a2+b2iz1±z2=(a1+b1i)±(a2+b2i)=(a1±a2)+(b1±b2)i

复数乘法
z 1 = a + b 1 i , z 2 = a + b 2 i z 1 z 2 = ( a 1 + b 1 i ) ( a 2 + b 2 i ) = ( a 1 a 2 − b 1 b 2 ) + ( a 1 b 2 + a 2 b 1 ) i z_1=a+b_1i,z_2=a+b_2i\\ z_1z_2=(a_1+b_1i)(a_2+b_2i)=(a_1a_2-b_1b_2)+(a_1b_2+a_2b_1)i z1=a+b1i,z2=a+b2iz1z2=(a1+b1i)(a2+b2i)=(a1a2b1b2)+(a1b2+a2b1)i

复数的几何意义:我们可以用一个直角坐标系的的 x x x轴来表示实部, y y y轴来表示虚部.即对于一个复数 z = a + b i z=a+bi z=a+bi,它在直角坐标系上表示为:
快速傅里叶变换FFT与快速数论变换NTT入门_第1张图片
欧拉公式(复数在指数上的定义) e ϕ i = cos ⁡ ϕ + i sin ⁡ ϕ e^{\phi i}=\cos \phi+i\sin \phi eϕi=cosϕ+isinϕ.

欧拉恒等式 e π i + 1 = 0 e^{\pi i}+1=0 eπi+1=0,即 e π i = − 1 e^{\pi i}=-1 eπi=1.

复数作为指数在极坐标系上的意义:对于 e u i = cos ⁡ u + i sin ⁡ u e^{ui}=\cos u+i\sin u eui=cosu+isinu,它在极坐标系上可以表示为:
快速傅里叶变换FFT与快速数论变换NTT入门_第2张图片


五.单位复数根.

单位复数根:一个 n n n次单位复数根定义为一个复数 ω \omega ω使得 ω n = 1 \omega^n=1 ωn=1,记为 ω n \omega_n ωn.

很明显对于任意一个正整数 n n n,有 n n n n n n次单位根恰好均匀分布在单位圆上.

例如 n = 8 n=8 n=8时:
快速傅里叶变换FFT与快速数论变换NTT入门_第3张图片
上图中 0 0 0 7 7 7分别表示 ω 8 0 \omega_{8}^0 ω80 ω 8 7 \omega_{8}^7 ω87.

根据欧拉定理和复数作为指数在极坐标系上的意义,可以得到:
ω n k = e 2 π i k n = ( e 2 π i n ) k \omega_{n}^{k}=e^{\frac{2\pi ik}{n}}=(e^{\frac{2\pi i}{n}})^{k} ωnk=en2πik=(en2πi)k

接下来我们给出三个引理:

引理1 ω d n d k = ω n k \omega_{dn}^{dk}=\omega_{n}^{k} ωdndk=ωnk.

证明: ω d n d k = e 2 π i d k d n = e 2 π i k n = ω n k \omega_{dn}^{dk}=e^{\frac{2\pi idk}{dn}}=e^{\frac{2\pi ik}{n}}=\omega_{n}^{k} ωdndk=edn2πidk=en2πik=ωnk.

引理2 ( ω n k + n 2 ) 2 = ( ω n k ) 2 (\omega_{n}^{k+\frac{n}{2}})^2=(\omega_{n}^{k})^2 (ωnk+2n)2=(ωnk)2.

证明: ( ω n k + n 2 ) 2 = ω n 2 k + n = ω n 2 k = ( ω n k ) 2 (\omega_{n}^{k+\frac{n}{2}})^2=\omega_{n}^{2k+n}=\omega_{n}^{2k}=(\omega_{n}^{k})^2 (ωnk+2n)2=ωn2k+n=ωn2k=(ωnk)2.

引理3:当 n ∣ k n|k nk时, ∑ j = 0 n − 1 ( ω n k ) j = 0 \sum_{j=0}^{n-1}(\omega_{n}^{k})^j=0 j=0n1(ωnk)j=0.

证明:
n ∣ k n|k nk时有:
∑ j = 0 n − 1 ( ω n k ) j = ( ω n k ) n − 1 ω n k − 1 = ( ω n n ) k − 1 ω n k − 1 = 1 k − 1 ω n k − 1 = 0 \sum_{j=0}^{n-1}(\omega_{n}^{k})^j=\frac{(\omega_n^k)^n-1}{\omega_n^k-1}=\frac{(\omega_n^n)^k-1}{\omega_n^k-1}=\frac{1^k-1}{\omega_n^k-1}=0 j=0n1(ωnk)j=ωnk1(ωnk)n1=ωnk1(ωnn)k1=ωnk11k1=0

证毕.


六.傅里叶变换DFT.

傅里叶变换专门指从系数表示到点值表示的过程,也就是说这一部分介绍点值过程.

接下来我们假设 n n n一定是 2 2 2的整次幂,这可以让分治的过程好理解也好写很多.

考虑三个多项式 A ( x ) , A 0 ( x ) , A 1 ( x ) A(x),A_0(x),A_1(x) A(x),A0(x),A1(x),它们的关系如下:
A ( x ) = ∑ i = 0 n − 1 a i x i = a 0 + a 1 x + a 2 x 2 + . . . + a n − 1 x n − 1 A 0 ( x ) = ∑ i = 0 n 2 − 1 a 2 i x 2 i = a 0 + a 2 x 2 + a 4 x 4 + . . . + a n − 2 x n − 2 A 1 ( x ) = ∑ i = 0 n 2 − 1 a 2 i + 1 x 2 i + 1 = a 1 x + a 3 x 3 + a 5 x 5 + . . . + a n − 1 x n − 1 A(x)=\sum_{i=0}^{n-1}a_ix^i=a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1}\\ A_0(x)=\sum_{i=0}^{\frac{n}{2}-1}a_{2i}x^{2i}=a_0+a_2x^2+a_4x^4+...+a_{n-2}x^{n-2}\\ A_1(x)=\sum_{i=0}^{\frac{n}{2}-1}a_{2i+1}x^{2i+1}=a_1x+a_3x^3+a_5x^5+...+a_{n-1}x^{n-1} A(x)=i=0n1aixi=a0+a1x+a2x2+...+an1xn1A0(x)=i=02n1a2ix2i=a0+a2x2+a4x4+...+an2xn2A1(x)=i=02n1a2i+1x2i+1=a1x+a3x3+a5x5+...+an1xn1

若我们用一个 n n n次单位根 ω n k \omega_n^k ωnk代入,则:
A ( ω n k ) = a 0 + a 1 ω n k + a 2 ω n 2 k + . . . + a n − 1 ω n ( n − 1 ) k = ( a 0 + a 2 ω n 2 k + a 4 ω n 4 k + . . . + a n − 2 ω n ( n − 2 ) k ) + ω n k ( a 1 + a 3 ω n 2 k + a 5 ω n 4 k + . . . + a n − 1 ω n ( n − 2 ) k ) = A 0 ( ω n 2 k ) + ω n k A 1 ( ω n 2 k ) A(\omega_n^k)=a_0+a_1\omega_n^k+a_2\omega_n^{2k}+...+a_{n-1}\omega_{n}^{(n-1)k}\\ =(a_0+a_2\omega_n^{2k}+a_4\omega_{n}^{4k}+...+a_{n-2}\omega_{n}^{(n-2)k})+\omega_n^k(a_1+a_3\omega_n^{2k}+a_5\omega_{n}^{4k}+...+a_{n-1}\omega_n^{(n-2)k})\\ =A_0(\omega_n^{2k})+\omega_{n}^kA_1(\omega_n^{2k}) A(ωnk)=a0+a1ωnk+a2ωn2k+...+an1ωn(n1)k=(a0+a2ωn2k+a4ωn4k+...+an2ωn(n2)k)+ωnk(a1+a3ωn2k+a5ωn4k+...+an1ωn(n2)k)=A0(ωn2k)+ωnkA1(ωn2k)

然后我们要分别往 A 0 , A 1 A_0,A_1 A0,A1里面代入 n 2 \frac{n}{2} 2n n n n次单位根,可是应该代入哪些单位根呢?

根据引理2,我们发现 ω n 2 k = ω n 2 k    m o d    n \omega_n^{2k}=\omega_n^{2k\,\,mod\,\,n} ωn2k=ωn2kmodn,也就是说实际上只有 n 2 \frac{n}{2} 2n个值而已.

根据引理1,发现 ω n 2 k    m o d    n = ω n 2 k    m o d    n 2 \omega_n^{2k\,\,mod\,\,n}=\omega_{\frac{n}{2}}^{k\,\,mod\,\,\frac{n}{2}} ωn2kmodn=ω2nkmod2n,所以其实就是代入 n 2 \frac{n}{2} 2n n 2 \frac{n}{2} 2n次单位根.

这样子分治很明显时间复杂度为 T ( n ) = 2 T ( n 2 ) + O ( n ) = O ( n log ⁡ n ) T(n)=2T(\frac{n}{2})+O(n)=O(n\log n) T(n)=2T(2n)+O(n)=O(nlogn).


七.傅里叶逆变换IDFT.

接下来我们讨论如何插值,即从点值表示到系数表示.

考虑上面的范德蒙矩阵,我们现在拥有的矩阵方程为:
[ 1 ω n ω n 2 ⋯ ω n n − 1 1 ω n 2 ω n 4 ⋯ ω n 2 ( n − 1 ) ⋮ ⋮ ⋮ ⋱ ⋮ 1 ω n n − 1 ω n 2 ( n − 1 ) ⋯ ω n ( n − 1 ) 2 ] [ a 0 a 1 ⋮ a n − 1 ] = [ F ( ω 0 ) F ( ω 1 ) ⋮ F ( ω n − 1 ) ] \left[\begin{matrix} 1&\omega_n&\omega_n^2&\cdots&\omega_n^{n-1}\\ 1&\omega_n^2&\omega_n^4&\cdots&\omega_n^{2(n-1)}\\ \vdots&\vdots&\vdots&\ddots&\vdots\\ 1&\omega_n^{n-1}&\omega_n^{2(n-1)}&\cdots&\omega_n^{(n-1)^2} \end{matrix}\right] \left[\begin{matrix} a_0\\ a_1\\ \vdots\\ a_{n-1}\\ \end{matrix}\right]= \left[\begin{matrix} F(\omega_0)\\ F(\omega_1)\\ \vdots\\ F(\omega_{n-1})\\ \end{matrix}\right] 111ωnωn2ωnn1ωn2ωn4ωn2(n1)ωnn1ωn2(n1)ωn(n1)2a0a1an1=F(ω0)F(ω1)F(ωn1)

现在我们用 V V V表示范德蒙矩阵,用 a ⃗ \vec{a} a 表示 a i a_i ai组成的矩阵,并且用 F F F表示等式右边的矩阵.

经过上面的讨论,我们要得到 V V V的逆矩阵 V − 1 V^{-1} V1来得到 a ⃗ \vec{a} a ,即 a ⃗ = V − 1 F \vec{a}=V^{-1}F a =V1F.

由于 V − 1 V = E V^{-1}V=E V1V=E E E E为单位矩阵满足:
E i , j = { 1 i = j 0 i ≠ j E_{i,j}= \left\{\begin{matrix} 1&i=j\\ 0&i\neq j \end{matrix}\right. Ei,j={10i=ji=j

定理:对于一个范德蒙矩阵 V = ( 1 , ω n , ω n 2 , . . . , ω n n − 1 ) V=(1,\omega_n,\omega_n^2,...,\omega_n^{n-1}) V=(1,ωn,ωn2,...,ωnn1) V j , k − 1 = ω n − k j n V^{-1}_{j,k}=\frac{\omega_n^{-kj}}{n} Vj,k1=nωnkj.

证明:
尝试证明 V V − 1 = E VV^{-1}=E VV1=E,考虑第 ( j , j ′ ) (j,j') (j,j)项:
( V − 1 V ) j , j ′ = ∑ k = 0 n − 1 V j , k − 1 V k , j ′ = ∑ k = 0 n − 1 ω n − k j n ω n k j ′ = ∑ k = 0 n − 1 ω n k ( j ′ − j ) n (V^{-1}V)_{j,j'}=\sum_{k=0}^{n-1}V^{-1}_{j,k}V_{k,j'}\\ =\sum_{k=0}^{n-1}\frac{\omega_n^{-kj}}{n}\omega_n^{kj'}\\ =\sum_{k=0}^{n-1}\frac{\omega_n^{k(j'-j)}}{n} (V1V)j,j=k=0n1Vj,k1Vk,j=k=0n1nωnkjωnkj=k=0n1nωnk(jj)

根据引理3可得:
( V − 1 V ) j , j ′ = { 1 j = j ′ 0 j ≠ j ′ = E j , j ′ (V^{-1}V)_{j,j'}= \left\{\begin{matrix} 1&j=j'\\ 0&j\neq j' \end{matrix}\right. =E_{j,j'} (V1V)j,j={10j=jj=j=Ej,j

证毕.

那么可以得到:
a j = 1 n ∑ k = 0 n − 1 F ( ω n k ) ω n − k j a_j=\frac{1}{n}\sum_{k=0}^{n-1}F(\omega_n^k)\omega_n^{-kj} aj=n1k=0n1F(ωnk)ωnkj

突然发现了什么,我们把 F ( ω n k ) F(\omega_n^k) F(ωnk)看成系数,把 ω n − k j \omega_n^{-kj} ωnkj看成 ω n − k \omega_n^{-k} ωnk的指数…

这不就是个卷积吗,FFT搞一下即可…


八.蝶形变换.

上面的FFT是递归而且要在函数内开数组,常数巨大而且容易爆栈,所以我们考虑能否写一个不用递归的FFT呢?

考虑递归的时候,把递归过程想象成一棵树,每一个节点所用的 a a a数组对应原来 a a a数组中的哪些下标:
快速傅里叶变换FFT与快速数论变换NTT入门_第4张图片
考虑自底向上一步步合并出原结果,那么我们要把这个东西按照叶子的位置重排一下.

这咋重排啊,这又没啥规律…搞成二进制看看吧:
0      000      000      0 4      100      001      1 2      010      010      2 6      110      011      3 1      001      100      4 5      101      101      5 3      011      110      6 7      111      111      7 0\,\,\,\,000\,\,\,\,000\,\,\,\,0\\ 4\,\,\,\,100\,\,\,\,001\,\,\,\,1\\ 2\,\,\,\,010\,\,\,\,010\,\,\,\,2\\ 6\,\,\,\,110\,\,\,\,011\,\,\,\,3\\ 1\,\,\,\,001\,\,\,\,100\,\,\,\,4\\ 5\,\,\,\,101\,\,\,\,101\,\,\,\,5\\ 3\,\,\,\,011\,\,\,\,110\,\,\,\,6\\ 7\,\,\,\,111\,\,\,\,111\,\,\,\,7 0000000041000011201001026110011310011004510110153011110671111117

突然发现叶子的顺序就是按照二进制翻转来的?那就二进制翻转好了.

怎么实现二进制翻转?考虑对于 i i i,把它向左移一位翻转后,再向左移一位,这是发现还有最高位可能为 1 1 1,判断原数最后一位是否为 1 1 1来判断是否要加.


九.快速数论变换NTT.

这一部分内容要涉及到原根,但不会涉及太多.想要详细了解原根可以去看高次同余方程之阶与原根,这里我们不详细展开了.

一个NTT模数一般来说要是 2 k x + 1 2^kx+1 2kx+1的形式,一般来说会是 998244353 , 1004535809 , 469762049 998244353,1004535809,469762049 998244353,1004535809,469762049,其中:
998244353 = 2 23 ∗ 119 + 1 1004535809 = 2 21 ∗ 479 + 1 469762049 = 2 26 ∗ 7 + 1 998244353=2^{23}*119+1\\ 1004535809=2^{21}*479+1\\ 469762049=2^{26}*7+1 998244353=223119+11004535809=221479+1469762049=2267+1

它们的原根都是 g = 3 g=3 g=3.

然后我们想起单位根要满足的性质为 ω n = 1 \omega^n=1 ωn=1,那么 ω n ≡ 1    ( m o d    p ) \omega^n\equiv 1\,\,(mod\,\,p) ωn1(modp),其中 p p p是个质数,现在我们要求 ω \omega ω.

很显然 ω = g p − 1 n \omega=g^{\frac{p-1}{n}} ω=gnp1,但是这时指数可不一定是个整数了,但是系数为 998244353 998244353 998244353 1004535809 1004535809 1004535809时, n = 2 20 n=2^{20} n=220时没有问题的,而 n > 2 20 n>2^{20} n>220的时候一般就不会让你NTT了.所以遇到这两个指数直接搞即可.

想要知道更多NTT模数和原根的可以去看FFT用到的各种素数.


十.例题与代码.

题目:UOJ34.

FFT:

#include
using namespace std;

typedef long long LL;

const int N=262144;
const double pi=acos(-1);

struct comp{
  double x,y;
  comp(double X=0,double Y=0){x=X;y=Y;}
  comp operator + (const comp &p)const{return comp(x+p.x,y+p.y);}
  comp operator - (const comp &p)const{return comp(x-p.x,y-p.y);}
  comp operator * (const comp &p)const{return comp(x*p.x-y*p.y,x*p.y+y*p.x);}
  comp operator * (const double &p)const{return comp(x*p,y*p);}
  comp operator / (const double &p)const{return comp(x/p,y/p);}
};

comp wn[2][N+9];

void Get_wn(){
  for (int i=0;i<N;++i){
	wn[0][i]=comp(cos(2*i*pi/N),sin(2*i*pi/N));
	wn[1][i]=comp(cos(2*i*pi/N),-sin(2*i*pi/N));
  }
}

int len,rev[N+9];

void Get_len(int n){
  int l=0;
  for (len=1;len<=n;len<<=1) ++l;
  for (int i=0;i<len;++i) rev[i]=rev[i>>1]>>1|(i&1)<<l-1;
}

comp pw[N+9];

void FFT(comp *a,int n,int t){
  for (int i=0;i<n;++i)
	if (i<rev[i]) swap(a[i],a[rev[i]]);
  for (int i=1;i<n;i<<=1){
	int tl=N/(i<<1);
	for (int j=0;j<i;++j) pw[j]=wn[t][j*tl];
	for (int j=0;j<n;j+=i<<1)
	  for (int k=0;k<i;++k){
		comp x=a[j+k],y=pw[k]*a[i+j+k];
		a[j+k]=x+y;a[i+j+k]=x-y;
	  }
  }
  if (!t) return;
  for (int i=0;i<n;++i) a[i]=a[i]/n;
}

void Poly_mul(comp *a,comp *b,int n){
  Get_len(n);
  FFT(a,len,0);
  FFT(b,len,0);
  for (int i=0;i<len;++i) a[i]=a[i]*b[i],b[i]=comp();
  FFT(a,len,1);
}

int n,m;
comp a[N+9],b[N+9];

void into(){
  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);
}

void work(){
  Get_wn();
  Poly_mul(a,b,n+m);
}

void outo(){
  for (int i=0;i<=n+m;++i)
	printf("%d ",(int)(a[i].x+0.5));
  puts("");
}

int main(){
  into();
  work();
  outo();
  return 0;
}

NTT:

#include
using namespace std;

typedef long long LL;

const int N=262144,mod=998244353,G=3,invG=332748118;

int add(int a,int b,int p=mod){return a+b>=p?a+b-p:a+b;}
int sub(int a,int b,int p=mod){return a-b<0?a-b+p:a-b;}
int mul(int a,int b,int p=mod){return (LL)a*b%p;}
void sadd(int &a,int b,int p=mod){a=add(a,b,p);}
void ssub(int &a,int b,int p=mod){a=sub(a,b,p);}
void smul(int &a,int b,int p=mod){a=mul(a,b,p);}
int Power(int a,int k,int p=mod){int res=1;for (;k;k>>=1,smul(a,a,p)) if (k&1) smul(res,a,p);return res;}
int Get_inv(int a,int p=mod){return Power(a,p-2,p);}

int wn[2][N+9];

void Get_wn(){
  int w0=Power(G,(mod-1)/N),w1=Power(invG,(mod-1)/N);
  wn[0][0]=wn[1][0]=1;
  for (int i=1;i<N;++i){
	wn[0][i]=mul(wn[0][i-1],w0);
	wn[1][i]=mul(wn[1][i-1],w1);
  }
}

int len,rev[N+9];

void Get_len(int n){
  int l=0;
  for (len=1;len<=n;len<<=1) ++l;
  for (int i=0;i<len;++i) rev[i]=rev[i>>1]>>1|(i&1)<<l-1;
}

int pw[N+9];

void NTT(int *a,int n,int t){
  for (int i=0;i<n;++i)
	if (i<rev[i]) swap(a[i],a[rev[i]]);
  for (int i=1;i<n;i<<=1){
	int tl=N/(i<<1);
	for (int j=0;j<i;++j) pw[j]=wn[t][j*tl];
	for (int j=0;j<n;j+=i<<1)
	  for (int k=0;k<i;++k){
		int x=a[j+k],y=(LL)pw[k]*a[i+j+k]%mod;
		a[j+k]=(x+y)%mod;a[i+j+k]=(x-y+mod)%mod;
	  }
  }
  if (!t) return;
  t=Get_inv(n);
  for (int i=0;i<n;++i) smul(a[i],t);
}

void Poly_mul(int *a,int *b,int n){
  Get_len(n);
  NTT(a,len,0);
  NTT(b,len,0);
  for (int i=0;i<len;++i) smul(a[i],b[i]),b[i]=0;
  NTT(a,len,1);
}

int n,m,a[N+9],b[N+9];

void into(){
  scanf("%d%d",&n,&m);
  for (int i=0;i<=n;++i)
	scanf("%d",&a[i]);
  for (int i=0;i<=m;++i)
	scanf("%d",&b[i]);
}

void work(){
  Get_wn();
  Poly_mul(a,b,n+m);
}

void outo(){
  for (int i=0;i<=n+m;++i)
	printf("%d ",a[i]);
  puts("");
}

int main(){
  into();
  work();
  outo();
  return 0;
}

写在最后.

感谢rvalue神犇的blog教会了我FFT.

《算法导论》上的FFT也不错,虽然我没看完.

你可能感兴趣的:(算法入门)