【luogu P3803】【模板】多项式乘法(FFT)

【模板】多项式乘法(FFT)

题目链接:luogu P3803

题目大意

给你两个多项式,要你求这两个多项式乘起来得到的多项式。(卷积)

思路

系数表示法

就是我们一般来表示一个多项式的方法:
A ( x ) = a 1 x k + a 2 x k − 1 + . . . + a k A(x)=a_1x^k+a_2x^{k-1}+...+a_k A(x)=a1xk+a2xk1+...+ak
或者可以这样表示:
A ( x ) = ∑ i = 1 k a i × x i A(x)=\sum\limits_{i=1}^{k}a_i\times x_i A(x)=i=1kai×xi

那你很容易看到,用来做这道题用系数表示法来做是 O ( n 2 ) O(n^2) O(n2) 的。

点值表示法

假设我们已经知道了这个多项式,那我们把 n n n 个不同的 x x x 带入,会得到 n n n 个取值 y y y
由于 n n n 个点可以确定一个 n n n 次多项式,那我们就可以根据这 n n n 个点确定这个多项式。

那你如果选了 n n n 个点 ( x 1 , y 1 ) , . . . , ( x n , y n ) (x_1,y_1),...,(x_n,y_n) (x1,y1),...,(xn,yn),那就有:
y i = ∑ j = 0 n − 1 a j × x i j y_i=\sum\limits_{j=0}^{n-1}a_j\times x_i^j yi=j=0n1aj×xij

但用它计算还是 O ( n 2 ) O(n^2) O(n2),你选点 O ( n ) O(n) O(n),对于每个点计算也是 O ( n ) O(n) O(n)

考虑优化

至于系数表示法,它的系数固定,你一改其它都要改,似乎很难弄。

但是第二种点值法似乎没有特别大的限制让它难以优化。
但是怎么优化呢?这就要看点前置知识才可以继续了。

前置知识

向量

普通的量只有大小,但是向量就是有方向又有大小的量。
在几何里面它其实就是一个箭头。

圆的弧度制

等于半径长的圆弧所对的圆心角叫做1弧度的角,叫做弧度,用 rad 表示。
用它做单位来量角的制度就是弧度制。

1 ∘ = π 180 r a d 1^{\circ}=\frac{\pi}{180}rad 1=180πrad
18 0 ∘ = π r a d 180^{\circ}=\pi rad 180=πrad

复数

a , b a,b a,b 为实数, i 2 = − 1 i^2=-1 i2=1,那形如 a + b i a+bi a+bi 的数就叫做复数。
i i i 就是其中的复数单位。

复数其实可以在一个二维平面表示出来, x x x 轴表示实数 a a a y y y 轴(当然在原点的时候不是虚数)表示虚数 b i bi bi
然后从 ( 0 , 0 ) (0,0) (0,0) ( a , b ) (a,b) (a,b) 的向量就表示了复数 a + b i a+bi a+bi

模长:就是那个点到原点的距离,勾股就可以得到。
幅角:从 x 正半轴以逆时针为正方向到一直向量的转角。

有关复数的运算法则

那由于复数在平面上是向量,那我们其实可以用向量的加减法则来弄。(就是用那个平行四边形定则)

然后乘法就稍微复杂一点。
几何的意义就是,复数相乘,模长也相乘,然后幅角就会相加。
其实我们这里要的是代数意义,我们化一下式子:
( a + b i ) ∗ ( c + d i ) (a+bi)*(c+di) (a+bi)(c+di)
= a c + a ∗ d i + b ∗ c i + b i ∗ d i =ac+a*di+b*ci+bi*di =ac+adi+bci+bidi
= a c + i ( a d + b c ) − b d =ac+i(ad+bc)-bd =ac+i(ad+bc)bd i 2 = − 1 i^2=-1 i2=1
= ( a c − b d ) + ( a d + b c ) i =(ac-bd)+(ad+bc)i =(acbd)+(ad+bc)i

单位根

在复平面上,以原点为圆心, 1 1 1 为半径作圆,所得的圆叫单位圆。

以圆点为起点,圆的 n n n 等分点为终点,做 n n n 个向量,设幅角为正且最小的向量对应的复数为 ω n \omega_n ωn,称为 n n n次单位根。

那剩下的那 n − 1 n-1 n1 个就是 ω n 2 , ω n 3 , . . . , ω n n \omega_n^2,\omega_n^3,...,\omega_n^n ωn2,ωn3,...,ωnn

然后要注意的就是 ω n 0 \omega_n^0 ωn0 ω n n \omega_n^n ωnn 都是 1 1 1,对于的是 x x x 正半轴的向量。

那怎么计算其它的呢?
我们可以用欧拉公式来求: ω n k = cos ⁡ k 2 π n + i sin ⁡ k 2 π n \omega_n^k=\cos k\dfrac{2\pi}{n}+i\sin k\dfrac{2\pi}{n} ωnk=coskn2π+isinkn2π
(大概就是把模长那条线往 x x x 轴做垂线得到直角三角形,然后用三角函数得到两条直角边的长度,从而得到坐标,也就是复数)

当然显而易见,单位根幅角就是 1 n \frac{1}{n} n1

还有就是如果 z n = 1 z^n=1 zn=1,那 z z z 就是 n n n 次单位根。

单位根的一些性质

ω 2 n 2 k = ω n k \omega_{2n}^{2k}=\omega_n^k ω2n2k=ωnk
证明:
ω 2 n 2 k = cos ⁡ 2 k 2 π 2 n + i sin ⁡ 2 k 2 π 2 n \omega_{2n}^{2k}=\cos 2k\dfrac{2\pi}{2n}+i\sin 2k\dfrac{2\pi}{2n} ω2n2k=cos2k2n2π+isin2k2n2π
= cos ⁡ k 2 π n + i sin ⁡ k 2 π n = ω n k =\cos k\dfrac{2\pi}{n}+i\sin k\dfrac{2\pi}{n}=\omega_n^k =coskn2π+isinkn2π=ωnk

ω n k + n 2 = − ω n k \omega_{n}^{k+\frac{n}{2}}=-\omega_{n}^{k} ωnk+2n=ωnk
证明:
ω n n 2 = cos ⁡ n 2 ∗ 2 π n + i sin ⁡ n 2 ∗ 2 π n \omega_{n}^{\frac{n}{2}}=\cos \dfrac{n}{2}*\dfrac{2\pi}{n}+i\sin \dfrac{n}{2}*\dfrac{2\pi}{n} ωn2n=cos2nn2π+isin2nn2π
= cos ⁡ π + i sin ⁡ π = − 1 =\cos \pi+i\sin\pi=-1 =cosπ+isinπ=1
cos ⁡ π = cos ⁡ 18 0 ∘ = − 1 , sin ⁡ π = sin ⁡ 18 0 ∘ = 0 \cos \pi=\cos 180^\circ=-1,\sin\pi=\sin 180^\circ=0 cosπ=cos180=1,sinπ=sin180=0
那就有 ω n k + n 2 = ω n k ∗ ω n n 2 = ω n k ∗ ( − 1 ) = − ω n k \omega_{n}^{k+\frac{n}{2}}=\omega_{n}^{k}*\omega_{n}^{\frac{n}{2}}=\omega_{n}^{k}*(-1)=-\omega_{n}^{k} ωnk+2n=ωnkωn2n=ωnk(1)=ωnk

OK,现在前置知识结束,开始正文。

快速傅里叶变换(FFT)

我们设 A ( x ) A(x) A(x) 的系数为 ( a 0 , a 1 , . . . , a n − 1 ) (a_0,a_1,...,a_{n-1}) (a0,a1,...,an1) n n n 次多项式)
那就有 A ( x ) = a 0 + a 1 x + a 2 x 2 + a 3 ∗ x 3 + a 4 ∗ x 4 + a 5 ∗ x 5 + ⋯ + a n − 2 ∗ x n − 2 + a n − 1 ∗ x n − 1 A(x)=a_0+a_1x+a_2{x^2}+a_3*{x^3}+a_4*{x^4}+a_5*{x^5}+ \dots+a_{n-2}*x^{n-2}+a_{n-1}*x^{n-1} A(x)=a0+a1x+a2x2+a3x3+a4x4+a5x5++an2xn2+an1xn1

我们可以把它奇偶分开, A ( x ) = ( a 0 + a 2 x 2 + . . . + a n − 2 x n − 2 ) + ( a 1 + a 3 x 3 + . . . + a n − 1 x n − 1 ) A(x)=(a_0+a_2x^2+...+a_{n-2}x^{n-2})+(a_1+a_3x^3+...+a_{n-1}x^{n-1}) A(x)=(a0+a2x2+...+an2xn2)+(a1+a3x3+...+an1xn1)
那我们可以设 A 1 ( x ) = a 0 + a 2 x + . . . + a n − 2 x n 2 − 1 , A 2 ( x ) = a 1 + a 3 x + . . . + a n − 1 x n 2 − 1 A_1(x)=a_0+a_2x+...+a_{n-2}x^{\frac{n}{2}-1},A_2(x)=a_1+a_3x+...+a_{n-1}x^{\frac{n}{2}-1} A1(x)=a0+a2x+...+an2x2n1,A2(x)=a1+a3x+...+an1x2n1

那我们容易看出 A ( x ) = A 1 ( x 2 ) + x A 2 ( x 2 ) A(x)=A_1(x^2)+xA_2(x^2) A(x)=A1(x2)+xA2(x2)

ω n k \omega_n^k ωnk 带入, A ( ω n k ) = A 1 ( ω n 2 k ) + ω n k A 2 ( ω n 2 k ) A(\omega_n^k)=A_1(\omega_n^{2k})+\omega_n^kA_2(\omega_n^{2k}) A(ωnk)=A1(ωn2k)+ωnkA2(ωn2k)

ω n k + n 2 \omega_n^{k+\frac{n}{2}} ωnk+2n 带入, A ( ω n k ) = A 1 ( ω n 2 k + n ) + ω n k + n 2 A 2 ( ω n 2 k + n ) A(\omega_n^k)=A_1(\omega_n^{2k+n})+\omega_n^{k+\frac{n}{2}}A_2(\omega_n^{2k+n}) A(ωnk)=A1(ωn2k+n)+ωnk+2nA2(ωn2k+n),然后把这个式子化一下:
= A 1 ( ω n 2 k ∗ ω n n ) − ω n k A 2 ( ω n 2 k ∗ ω n n ) =A_1(\omega_n^{2k}*\omega_n^n)-\omega_n^{k}A_2(\omega_n^{2k}*\omega_n^n) =A1(ωn2kωnn)ωnkA2(ωn2kωnn)
= A 1 ( ω n 2 k ) − ω n k A 2 ( ω n 2 k ) =A_1(\omega_n^{2k})-\omega_n^{k}A_2(\omega_n^{2k}) =A1(ωn2k)ωnkA2(ωn2k)

你会发现这两个东西不同的只有一个常数项。
那我们可以枚举得到第一个答案,然后通过第一个答案得到第二个。
那你会发现你只用枚举前面的一半,那问题规模就减半。

那分出的两个问题可以继续分半解决,那我们可以用递归来搞。
那这个其实就是类似分治的感觉,是 O ( n l o g n ) O(nlogn) O(nlogn) 的。

快速傅里叶逆变换(IFFT)

前面我们在弄的时候都是用点值表示法。

但一般我们(题目)给的多半都是系数表示法。
从系数表示法得到点值表示法我们已经学会了,但是怎么转回来呢?

这个时候就要用到 IFFT 了。
( y 0 , y 1 , . . . , y n − 1 ) (y_0,y_1,...,y_{n-1}) (y0,y1,...,yn1) ( a 0 , a 1 , . . . , a n − 1 ) (a_0,a_1,...,a_{n-1}) (a0,a1,...,an1) 的傅里叶变换(就是点值表示)
那另外有一个 ( c 0 , c 1 , . . . , c n − 1 ) (c_0,c_1,...,c_{n-1}) (c0,c1,...,cn1) 满足:
c k = ∑ i = 0 n − 1 y i ( ω n − k ) i c_k=\sum\limits_{i=0}^{n-1}y_i(\omega_n^{-k})^i ck=i=0n1yi(ωnk)i
(其实就是把 ( y 0 , y 1 , . . . , y n − 1 ) (y_0,y_1,...,y_{n-1}) (y0,y1,...,yn1) 当做多项式,求它在 ω n 0 , ω n − 1 , . . . , ω n − ( n − 1 ) \omega_n^0,\omega_n^{-1},...,\omega_n^{-(n-1)} ωn0,ωn1,...,ωn(n1) 的点值表示)

然后来推推公式:
c k = ∑ i = 0 n − 1 y i ( ω n − k ) i c_k=\sum\limits_{i=0}^{n-1}y_i(\omega_n^{-k})^i ck=i=0n1yi(ωnk)i
= ∑ i = 0 n − 1 ( ∑ j = 0 n − 1 a j ( ω n i ) j ) ( ω n − k ) i =\sum\limits_{i=0}^{n-1}(\sum\limits_{j=0}^{n-1}a_j(\omega_n^i)^j)(\omega_n^{-k})^i =i=0n1(j=0n1aj(ωni)j)(ωnk)i
= ∑ i = 0 n − 1 ( ∑ j = 0 n − 1 a j ( ω n j ) i ) ( ω n − k ) i =\sum\limits_{i=0}^{n-1}(\sum\limits_{j=0}^{n-1}a_j(\omega_n^j)^i)(\omega_n^{-k})^i =i=0n1(j=0n1aj(ωnj)i)(ωnk)i
= ∑ i = 0 n − 1 ( ∑ j = 0 n − 1 a j ( ω n j ) i ( ω n − k ) i ) =\sum\limits_{i=0}^{n-1}(\sum\limits_{j=0}^{n-1}a_j(\omega_n^j)^i(\omega_n^{-k})^i) =i=0n1(j=0n1aj(ωnj)i(ωnk)i)
= ∑ i = 0 n − 1 ∑ j = 0 n − 1 a j ( ω n j − k ) i =\sum\limits_{i=0}^{n-1}\sum\limits_{j=0}^{n-1}a_j(\omega_n^{j-k})^i =i=0n1j=0n1aj(ωnjk)i
= ∑ j = 0 n − 1 a j ∑ i = 0 n − 1 ( ω n j − k ) i =\sum\limits_{j=0}^{n-1}a_j\sum\limits_{i=0}^{n-1}(\omega_n^{j-k})^i =j=0n1aji=0n1(ωnjk)i

我们设 S ( x ) = ∑ i = 0 n − 1 x i S(x)=\sum\limits_{i=0}^{n-1}x^i S(x)=i=0n1xi
带入 ω n k \omega_n^k ωnk S ( ω n k ) = ∑ i = 0 n − 1 ( ω n k ) i S(\omega_n^k)=\sum\limits_{i=0}^{n-1}(\omega_n^k)^i S(ωnk)=i=0n1(ωnk)i

然后分类讨论:

k = 0 k=0 k=0 时, ω n k = ω n 0 = 1 \omega_n^k=\omega_n^0=1 ωnk=ωn0=1
那式子就变成了 S ( ω n k ) = ∑ i = 0 n − 1 1 i = n S(\omega_n^k)=\sum\limits_{i=0}^{n-1}1^i=n S(ωnk)=i=0n11i=n

k ≠ 0 k\neq0 k=0 时,我们考虑怎么求。
观察到没项都乘了一个值,我们考虑用这么一种方法:
ω n k \omega_n^k ωnk ω n k S ( ω n k ) = ∑ i = 1 n ( ω n k ) i \omega_n^kS(\omega_n^k)=\sum\limits_{i=1}^{n}(\omega_n^k)^i ωnkS(ωnk)=i=1n(ωnk)i
然后两个相减: ω n k S ( ω n k ) − S ( ω n k ) = ( ω n k ) n − ( w n k ) 0 \omega_n^kS(\omega_n^k)-S(\omega_n^k)=(\omega_n^k)^n-(w_n^k)^0 ωnkS(ωnk)S(ωnk)=(ωnk)n(wnk)0
( ω n k − 1 ) S ( ω n k ) = ( ω n k ) n − 1 (\omega_n^k-1)S(\omega_n^k)=(\omega_n^k)^n-1 (ωnk1)S(ωnk)=(ωnk)n1
( ω n k − 1 ) S ( ω n k ) = ( ω n n ) k − 1 (\omega_n^k-1)S(\omega_n^k)=(\omega_n^n)^k-1 (ωnk1)S(ωnk)=(ωnn)k1
( ω n k − 1 ) S ( ω n k ) = 1 k − 1 (\omega_n^k-1)S(\omega_n^k)=1^k-1 (ωnk1)S(ωnk)=1k1
S ( ω n k ) = 1 − 1 ω n k − 1 = 0 S(\omega_n^k)=\dfrac{1-1}{\omega_n^k-1}=0 S(ωnk)=ωnk111=0

OK,分析完了 S ( x ) S(x) S(x),我们回到之前卡克的地方:
c k = ∑ j = 0 n − 1 a j ∑ i = 0 n − 1 ( ω n j − k ) i c_k=\sum\limits_{j=0}^{n-1}a_j\sum\limits_{i=0}^{n-1}(\omega_n^{j-k})^i ck=j=0n1aji=0n1(ωnjk)i
= ∑ j = 0 n − 1 a j S ( ω n j − k ) =\sum\limits_{j=0}^{n-1}a_jS(\omega_n^{j-k}) =j=0n1ajS(ωnjk)
那只会有一个 j = k j=k j=k,使得 j − k = 0 j-k=0 jk=0,贡献是 n n n,其它的时候,贡献都是 0 0 0
那就是 c k = a k n c_k=a_kn ck=akn
那你就会发现, a k = c k n a_k=\dfrac{c_k}{n} ak=nck

那我们就可以通过求出 ( c 0 , c 1 , . . . , c n − 1 ) (c_0,c_1,...,c_{n-1}) (c0,c1,...,cn1),然后再根据这个一弄就可以得到 a k a_k ak 了。
(而且你会发现求 ( c 0 , c 1 , . . . , c n − 1 ) (c_0,c_1,...,c_{n-1}) (c0,c1,...,cn1) 和求 FFT 的很像,只是由 ω n i \omega_n^i ωni 变成了 ω n − i \omega_n^{-i} ωni,那我们只要再搞一个 o p op op,记录它单位的改变即可以了)

算法总结

它的主要思想就是把系数表示法转成点值表示法,然后用点值表示法的分支方法来快速得到答案,然后再将答案转回系数表示法。

实现

妹想到把,还有。

别的地方都没有问题,我们来讲讲递归的做法。

递归

(我一定不会告诉你这个是过不了的)

递归其实就是根据我们前面的奇偶分开做法,把一个序列弄成两个,然后递归处理之后合并。

递归到只剩一个常数项的时候,就可以直接返回。

然后我们先来一个小小的优化。
它有一个很牛逼的名字——蝴蝶效应。
(其实就是记忆化)
因为向量的乘法耗比较多时间,我们可以先把要乘的乘出来放一个变量里,然后要用的时候直接就是这个变量。
(这样如果这个乘法值要用多次的话就可以节省时间,原本要乘很多次,现在只用乘一次)

然而就算你加了这个优化,也是过不了的。
因为你递归,加上常数比较大,在 1 0 6 10^6 106 就爆了。

那我们就要找一个更优的方法来弄,那就是迭代实现。

迭代实现

我们考虑观察一下原序列和翻转之后的序列。

我们观察一下它是怎么变的:
0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 0,1,2,3,4,5,6,7 0,1,2,3,4,5,6,7
0 , 2 , 4 , 6 , 1 , 3 , 5 , 7 0,2,4,6,1,3,5,7 0,2,4,6,1,3,5,7
0 , 4 , 2 , 6 , 1 , 5 , 3 , 7 0,4,2,6,1,5,3,7 0,4,2,6,1,5,3,7

似乎没有什么想法,转成二进制:
000 , 100 , 010 , 110 , 001 , 101 , 011 , 111 000,100,010,110,001,101,011,111 000,100,010,110,001,101,011,111
再把一开始的也转成二进制:
000 , 001 , 010 , 011 , 100 , 101 , 110 , 111 000,001,010,011,100,101,110,111 000,001,010,011,100,101,110,111
你会发现它就是把它们的二进制翻转了。

那你可以用一个 O ( n ) O(n) O(n) 的方法得到对应关系——DP。
f i = ( f i / 2 / 2 ) ∣ ( ( i & 1 ) n − 1 ) f_i=(f_{i/2}/2)|((i\&1)^{n-1}) fi=(fi/2/2)((i&1)n1)
大概就是把之前翻转好的往左边移一个,流出位置给原序列中右边新出现的一位放。

接着就是怎么通过这个翻转对应得到我们迭代的序列。
现有一个显然的东西,就是 f f i = i f_{f_i}=i ffi=i

那也就是说,它们之间是两两对应的。
那就需要把每对都只翻转一次,就可以了。
那简单,我们只要找一个在一对里面只会发生一次的事,就比如 f i > i f_i>i fi>i。(当然你小于也可以,只要让一对只有一个会发生就可以了)

然后我们来讲讲迭代要怎么搞。
其实就想到与从下段直接网上合并。(因为你已经确定了最下的序列)
就枚举合并的大小,然后枚举区间,然后就合并。

小声哔哔

终于写完了,好累啊。
awa

代码

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

using namespace std;

struct complex {
	double x, y;
	complex (double xx = 0, double yy = 0) {
		x = xx;
		y = yy;
	}
}a[5000005], b[5000005];
int n, m, limit, l_size;
int an[5000005];
double Pi = acos(-1.0);

complex operator +(complex x, complex y) {//定义向量的加减乘
	return complex(x.x + y.x, x.y + y.y);
}

complex operator -(complex x, complex y) {
	return complex(x.x - y.x, x.y - y.y);
}

complex operator *(complex x, complex y) {
	return complex(x.x * y.x - x.y * y.y, x.x * y.y + x.y * y.x);
}

//op=1则系数变点值,为-1则点值边系数
//至于为啥看看求的两个公式就知道了
void FFT(complex *now, int op) {
	for (int i = 0; i < limit; i++)
		if (i < an[i]) swap(now[i], now[an[i]]);//求出要迭代的序列
	
	for (int mid = 1; mid < limit; mid <<= 1) {//枚举合并序列的大小
		complex Wn(cos(Pi / mid), op * sin(Pi / mid));//单位根
		for (int R = mid << 1, j = 0; j < limit; j += R) {//R是区间右端点,j表示左端点
			complex w(1, 0);//幂
			for (int k = 0; k < mid; k++, w = w * Wn) {//枚举区间的每个位置
				complex x = now[j + k], y = w * now[j + mid + k];//先求出来减少时间
				now[j + k] = x + y;//得到一个求出另外一个的
				now[j + mid + k] = x - y;
			}
		}
	}
}

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);
	
	limit = 1;
	while (limit <= n + m) {
		limit <<= 1;
		l_size++;
	}
	
	for (int i = 0; i < limit; i++)
		an[i] = (an[i >> 1] >> 1) | ((i & 1) << (l_size - 1));
	
	FFT(a, 1);
	FFT(b, 1);
	
	for (int i = 0; i <= limit; i++)
		a[i] = a[i] * b[i];
	
	//将点值表示法转换为系数表示法再输出
	FFT(a, -1);
	
	for (int i = 0; i <= n + m; i++)
		printf("%d ", (int)(a[i].x / limit + 0.5));
	
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值