文章目录
〇、前言
虽然听得心态爆炸, 但是还好的是没有 y m x ymx ymx 大佬的飞机开得好…
至少我还没有坐飞机…
u
p
d
a
t
e
.
2021.2.2
\tt update.2021.2.2
update.2021.2.2 改了一下排版以及一些可能有点问题的地方. 并且把它宅到新博客上去了
壹、啥是 FFT ?它可以干什么?
首先,你需要知道 矩阵乘法 的相关知识。
通过 矩阵乘法 的知识,我们知道,对于一个 f ( x ) f(x) f(x) 与 g ( x ) g(x) g(x) ,如果我们用朴素的矩阵乘法计算 f ( x ) ⋅ g ( x ) f(x)\cdot g(x) f(x)⋅g(x) ,时间复杂度是 O ( N 2 M ) O(N^2M) O(N2M) ,但是对于这是函数,复杂度中的 M = 1 M=1 M=1 ,那么时间复杂度为 O ( N 2 ) O(N^2) O(N2) 。
似乎这是比较优秀的,但是看看这道 板题 :
嗯?
n
,
m
≤
1
0
6
n,m\le 10^6
n,m≤106 ,似乎过于太小了?(不要管这病句…)
而 F F T \tt FFT FFT 就是用来解决多项式乘法的问题的,可以把它的时间复杂度优化到 O ( n log n ) O(n\log n) O(nlogn).
这里引用一句话:
实际上, F F T \tt FFT FFT 并不是直接计算多项式乘法,而是把原来的多项式 f ( x ) , g ( x ) f(x),g(x) f(x),g(x) 在 O ( n log n ) \mathcal O(n\log n) O(nlogn) 的复杂度内转换为它的 点值表示(后面会讲),而点值表示的多项式相乘的时间复杂度是 O ( n ) \mathcal O(n) O(n) 的。最后再用 O ( n log n ) O(n\log n) O(nlogn) 的时间复杂度把所得多项式的点值表示转化为一般形式。
贰、必备芝士
在学习 F F T \tt FFT FFT 之前,我们需要知道 点值表示 和 复数 。
一、点值表示
1.1.什么是点值表示
对于一个函数 f ( x ) f(x) f(x) ,我们可以用一个多项式表示它:
f ( x ) = a n x n + a n − 1 x n − 1 + . . . + a 1 x + a 0 f(x)=a_nx^n+a_{n-1}x^{n-1}+...+a_{1}x+a_0 f(x)=anxn+an−1xn−1+...+a1x+a0.
这种表达方法我们叫做 一般形式.
如果熟悉,那么可以确定一个东西,如果我们有在这个函数上的 n + 1 n+1 n+1 个点的具体的坐标 ( x i , y i ) (x_i,y_i) (xi,yi) ,那么我们就可以唯一确定这个函数的解析式,那么,我们也可以用这 n + 1 n+1 n+1 个点的具体坐标来表示这个多项式。
当然,如果知道 拉格朗日插值法 的大佬,或许可以更好地理解这句话。
也就是说一个多项式与一个点值表示是一一对应的。
那么 FFT 完成的操作就是:
- 把已知的一个多项式转化成对应的点值表示;
- 把已知的点值表示转换成对应的多项式。
复杂度都是 O ( n log n ) O(n\log n) O(nlogn).
1.2.点值表示的乘法
那么,假设我们用 { x 0 , x 1 , x 2 , . . . , x n } \{x_0,x_1,x_2,...,x_n\} {x0,x1,x2,...,xn} 来表示 f ( x ) f(x) f(x) 为 { ( x 0 , f ( x 0 ) ) , ( x 1 , f ( x 1 ) ) , . . . , ( x n , f ( x n ) ) } \{(x_0,f(x_0)),(x_1,f(x_1)),...,(x_n,f(x_n))\} {(x0,f(x0)),(x1,f(x1)),...,(xn,f(xn))} ,表示 g ( x ) g(x) g(x) 为 { ( x 0 , g ( x 0 ) ) , ( x 1 , g ( x 1 ) ) , . . . , ( x n , g ( x n ) ) } \{(x_0,g(x_0)),(x_1,g(x_1)),...,(x_n,g(x_n))\} {(x0,g(x0)),(x1,g(x1)),...,(xn,g(xn))} 。
那么我们就知道 f ( x ) ⋅ g ( x ) f(x)\cdot g(x) f(x)⋅g(x) 的点值表达式为 { ( x 0 , f ( x 0 ) ⋅ g ( x 0 ) ) , . . . , ( x n , f ( x n ) ⋅ g ( x n ) ) } \{(x_0,f(x_0)\cdot g(x_0)),...,(x_n,f(x_n)\cdot g(x_n))\} {(x0,f(x0)⋅g(x0)),...,(xn,f(xn)⋅g(xn))} ,其实就是对应项相乘,那么这个的时间复杂度为 O ( n ) \mathcal O(n) O(n) ,似乎可以接受.
二、复数
2.1.复数的定义
H i n t . Hint. Hint. 是 复数(complex) 而不是 负数(negative) 。
首先我们要知道 虚数 ,即有一个很特别的数 i i i ,满足 i 2 = − 1 i^2=-1 i2=−1 ,即 i = − 1 i=\sqrt{-1} i=−1.
不用理解为什么,因为他本来就是特殊定义的,只需要记住就好。
然后一个复数 a a a 可以表示为实部 x x x 和虚部 y × i y\times i y×i 之和—— a = x + y × i a=x+y\times i a=x+y×i 。对于一个实数,我们可以把它放在“一维数轴”上,即数轴;那么对于一个复数,我们需要把它放在“二维数轴”,也就是直角坐标系上。
其实,我们可以把 a a a 表示成 向量(vector) 的形式,即将 a = x + y × i a=x+y\times i a=x+y×i 表示为向量 ( x , y ) (x,y) (x,y) ,但是对于后面虚数的乘法,其实是不满足向量乘法的,但是这可以作为一个理解方式。
2.2.复数的乘法运算
对于两个虚数 a , b a,b a,b ,其中 a = x + y × i , b = x ′ + y ′ × i a=x+y\times i,b=x'+y'\times i a=x+y×i,b=x′+y′×i ,计算 a b ab ab.
我们有两种方法:
- 可以直接展开相乘: a b = ( x + y i ) ( x ′ + y ′ i ) = ( x x ′ − y y ′ ) + ( x y ′ + x ′ y ) i ab=(x+yi)(x'+y'i)=(xx'-yy')+(xy'+x'y)i ab=(x+yi)(x′+y′i)=(xx′−yy′)+(xy′+x′y)i.
- 可以从几何角度理解:两个复数相乘等于“幅角相加,模长相乘”(但并不满足向量的运算)
更推荐就用方法一来理解。
2.3.单位复数
H i n t . Hint. Hint. 复数的模长 ∣ a ∣ |a| ∣a∣ (这个不是绝对值),表示把它表示成向量后的长度,即 ∣ a ∣ = x 2 + y 2 |a|=\sqrt{x^2+y^2} ∣a∣=x2+y2.
我们定义 n n n 次单位复根 ω n i \omega_n^i ωni 为满足 z n = 1 \text{z}^n=1 zn=1 的复数 z \text{z} z.
本人的理解方法:单位复根 ω n i \omega_n^i ωni 即为将一个单位圆分成 n n n 分,而 ω n i \omega_n^i ωni 就是 i i i 个这样的角拼起来。特别地, ω n 0 = 1 \omega_n^0=1 ωn0=1.
其实,之后的理解都可以通过 三角函数 有更好的理解.
可以发现 n n n 次单位复根满足以下性质( ω n i \omega_n^i ωni 表示第 i i i 个单位复根):
- ∣ ω n i ∣ = 1 |\omega_n^i|=1 ∣ωni∣=1 (都在单位圆上);
- ω n i \omega_n^i ωni 的幅角为 2 π i n \frac{2\pi i}{n} n2πi ; T a b . Tab. Tab. 这里的 i i i 是下标不是虚数,角度为弧度制( 36 0 ∘ 360^{\circ} 360∘ 弧度角为 2 π 2\pi 2π )
- 总共有 n n n 个单位复根,记为 ω n 0 , ω n 1 , ω n 2 , . . . , ω n n − 1 \omega_n^0,\omega_n^1,\omega_n^2,...,\omega_n^{n-1} ωn0,ωn1,ωn2,...,ωnn−1
另外,还有一些比较明显的性质:
- ω n i ⋅ ω n j = ω n i + j \omega_n^i\cdot\omega_n^j=\omega_n^{i+j} ωni⋅ωnj=ωni+j ;
- ω n i + n = ω n i \omega_n^{i+n}=\omega_n^i ωni+n=ωni ;
- ω n i = ω k n k i \omega_n^i=\omega_{kn}^{ki} ωni=ωknki ;
- ω n i = − ω n i + n / 2 \omega_n^i=-\omega_n^{i+n/2} ωni=−ωni+n/2 (这里的“负”表示大小相等、方向相反) ;
以上这些结论都可以通过在单位圆上画出单位根来证明。
单位复根有什么用呢?因为 n n n 次单位复根恰好有 n n n 个,就可以把这 n n n 个单位复根分别代入 n − 1 n-1 n−1 次多项式 f ( x ) f(x) f(x) —— 得到点值表达 { ( ω n 0 , f ( ω n 0 ) ) , ( ω n 1 , f ( ω n 1 ) ) , . . . , ( ω n n − 1 , f ( ω n n − 1 ) ) } \{(\omega_n^0,f(\omega_n^0)),(\omega_n^1,f(\omega_n^1)),...,(\omega_n^{n-1},f(\omega_n^{n-1}))\} {(ωn0,f(ωn0)),(ωn1,f(ωn1)),...,(ωnn−1,f(ωnn−1))}.
至于为什么要用单位复根作为点值表达式的基础?这是由于它的一些特性,其实如果能找到其他性质相似的数也是可以的.
叁、傅立叶正变换
有了这些辅助知识,我们终于可以进行正题了。
所谓变换,那么一定有正也有逆,现在我我们先来掌握它的正变换。
F F T \tt FFT FFT 的正变换实现,是基于对多项式进行奇偶项分开递归再合并的分治进行的。
对于 n − 1 n-1 n−1 次多项式,我们选择插入 n n n 次单位根求出其点值表达式。
记多项式 A ( x ) = a 0 + a 1 x + a 2 x 2 + . . . + a n − 1 x n − 1 A(x)=a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1} A(x)=a0+a1x+a2x2+...+an−1xn−1 .
再记 A o ( x ) = a 1 + a 3 x + a 5 x 2 + . . . A_o(x)=a_1+a_3x+a_5x^2+... Ao(x)=a1+a3x+a5x2+...,即将原函数奇数次系数提出来作为新的函数.
再记 A e ( x ) = a 0 + a 2 x + a 4 x 2 + . . . A_e(x)=a_0+a_2x+a_4x^2+... Ae(x)=a0+a2x+a4x2+...,即将原函数偶数次系数提出来作为新的函数.
有 A ( x ) = x ∗ A o ( x 2 ) + A e ( x 2 ) A(x)=x*A_o(x^2)+A_e(x^2) A(x)=x∗Ao(x2)+Ae(x2).
令 n = 2 p n = 2p n=2p 。则有:
A ( ω n k ) = ω n k × A o [ ( ω n / 2 k / 2 ) 2 ] + A e [ ( ω n / 2 k / 2 ) 2 ] = ω n k × A o ( ω p k ) + A e ( ω p k ) A(\omega_n^k)=\omega_n^k\times A_o[(\omega_{n/2}^{k/2})^2]+A_e[(\omega_{n/2}^{k/2})^2]=\omega_n^k\times A_o(\omega_p^k)+A_e(\omega_p^k) A(ωnk)=ωnk×Ao[(ωn/2k/2)2]+Ae[(ωn/2k/2)2]=ωnk×Ao(ωpk)+Ae(ωpk).
A ( ω n k + p ) = ω n k + p × A o ( w p k + p ) + A e ( w p k + p ) = − ω n k × A o ( w p k ) + A e ( w p k ) A(\omega_n^{k+p})=\omega_n^{k+p}\times A_o(w_p^{k+p})+A_e(w_p^{k+p})=-\omega_n^k\times A_o(w_p^k)+A_e(w_p^k) A(ωnk+p)=ωnk+p×Ao(wpk+p)+Ae(wpk+p)=−ωnk×Ao(wpk)+Ae(wpk).
此处用到 ω n k = ω n x − 1 k x − 1 = ω n / x k / x \omega_n^k=\omega_{nx^{-1}}^{kx^{-1}}=\omega_{n/x}^{k/x} ωnk=ωnx−1kx−1=ωn/xk/x 这个性质.
因此,在已知 A o ( w p k ) A_o(w_p^k) Ao(wpk) 与 A e ( w p k ) A_e(w_p^k) Ae(wpk) 的前提下,可以 O ( 1 ) O(1) O(1) 算出 A ( ω n k ) A(\omega_n^k) A(ωnk) 与 A ( ω n k + p ) A(\omega_n^{k+p}) A(ωnk+p).
因此,假如我们递归求解 A o ( x ) , A e ( x ) A_o(x),A_e(x) Ao(x),Ae(x) 两个多项式 p p p 次单位根的插值,就可以在 O ( n ) O(n) O(n) 的时间内算出 A ( x ) A(x) A(x) 的 n n n 次单位根的插值。
时间复杂度是经典的 T ( n ) = 2 × T ( n / 2 ) + O ( n ) = O ( n log n ) \mathcal T(n)=2\times \mathcal T(n/2)+\mathcal O(n)=\mathcal O(n\log n) T(n)=2×T(n/2)+O(n)=O(nlogn).
肆、傅里叶逆变换
刚刚研究完正的,现在我们来研究逆变换,其实也比较好理解。
观察我们刚刚的插值过程,实际上就是进行了如下的矩阵乘法。
[ ( ω n 0 ) 0 ( ω n 0 ) 1 ⋯ ( ω n 0 ) n − 1 ( ω n 1 ) 0 ( ω n 1 ) 1 ⋯ ( ω n 1 ) n − 1 ⋮ ⋮ ⋱ ⋮ ( ω n n − 1 ) 0 ( ω n n − 1 ) 1 ⋯ ( ω n n − 1 ) n − 1 ] [ a 0 a 1 ⋮ a n − 1 ] = [ A ( ω n 0 ) A ( ω n 1 ) ⋮ A ( ω n n − 1 ) ] \begin{bmatrix} (\omega_n^0)^0 & (\omega_n^0)^1 & \cdots & (\omega_n^0)^{n-1} \\ (\omega_n^1)^0 & (\omega_n^1)^1 & \cdots & (\omega_n^1)^{n-1} \\ \vdots & \vdots & \ddots & \vdots \\ (\omega_n^{n-1})^0 & (\omega_n^{n-1})^1 & \cdots & (\omega_n^{n-1})^{n-1} \end{bmatrix} \begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{n-1} \end{bmatrix} = \begin{bmatrix} A(\omega_n^0) \\ A(\omega_n^1) \\ \vdots \\ A(\omega_n^{n-1}) \end{bmatrix} ⎣⎢⎢⎢⎡(ωn0)0(ωn1)0⋮(ωnn−1)0(ωn0)1(ωn1)1⋮(ωnn−1)1⋯⋯⋱⋯(ωn0)n−1(ωn1)n−1⋮(ωnn−1)n−1⎦⎥⎥⎥⎤⎣⎢⎢⎢⎡a0a1⋮an−1⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡A(ωn0)A(ωn1)⋮A(ωnn−1)⎦⎥⎥⎥⎤
我们记上面的 ω \omega ω 矩阵为 V V V(就是最左边的矩阵).
对于下面定义的 D D D :
D = [ ( ω n − 0 ) 0 ( ω n − 0 ) 1 ⋯ ( ω n − 0 ) n − 1 ( ω n − 1 ) 0 ( ω n − 1 ) 1 ⋯ ( ω n − 1 ) n − 1 ⋮ ⋮ ⋱ ⋮ ( ω n − ( n − 1 ) ) 0 ( ω n − ( n − 1 ) ) 1 ⋯ ( ω n − ( n − 1 ) ) n − 1 ] D = \begin{bmatrix} (\omega_n^{-0})^0 & (\omega_n^{-0})^1 & \cdots & (\omega_n^{-0})^{n-1} \\ (\omega_n^{-1})^0 & (\omega_n^{-1})^1 & \cdots & (\omega_n^{-1})^{n-1} \\ \vdots & \vdots & \ddots & \vdots \\ (\omega_n^{-(n-1)})^0 & (\omega_n^{-(n-1)})^1 & \cdots & (\omega_n^{-(n-1)})^{n-1} \end{bmatrix} D=⎣⎢⎢⎢⎡(ωn−0)0(ωn−1)0⋮(ωn−(n−1))0(ωn−0)1(ωn−1)1⋮(ωn−(n−1))1⋯⋯⋱⋯(ωn−0)n−1(ωn−1)n−1⋮(ωn−(n−1))n−1⎦⎥⎥⎥⎤
考虑 D × V D\times V D×V 的结果:
( D ∗ V ) i j = ∑ k = 0 k < n d i k × v k j = ∑ k = 0 k < n ω n − i k × w n k j = ∑ k = 0 k < n ω n k ( j − i ) (D*V)_{ij} =\sum_{k=0}^{k<n}d_{ik}\times v_{kj} =\sum_{k=0}^{k<n}\omega_n^{-ik}\times w_{n}^{kj} =\sum_{k=0}^{k<n}\omega_n^{k(j-i)} (D∗V)ij=k=0∑k<ndik×vkj=k=0∑k<nωn−ik×wnkj=k=0∑k<nωnk(j−i)
当 i = j i = j i=j 时, ( D × V ) i j = n (D\times V)_{ij}=n (D×V)ij=n ;
当 i ≠ j i ≠ j i=j 时, ( D × V ) i j = 1 + ω n j − i + ( ω n j − i ) 2 + . . . = 1 − ( ω n j − i ) n 1 − ω n j − i = 0 (D\times V)_{ij}=1+\omega_n^{j-i}+(\omega_n^{j-i})^2+...=\frac{1-(\omega_n^{j-i})^n}{1-\omega_n^{j-i}}=0 (D×V)ij=1+ωnj−i+(ωnj−i)2+...=1−ωnj−i1−(ωnj−i)n=0 ;
根据定义, n n n 次单位根的 n n n 次方都等于 1 1 1.
所以: 1 n × D = V − 1 \frac1n\times D=V^{-1} n1×D=V−1,即我们得到其逆矩阵.
因此将这个结果代入最上面那个公式里面,有:
[ a 0 a 1 ⋮ a n − 1 ] = 1 n [ ( ω n − 0 ) 0 ( ω n − 0 ) 1 ⋯ ( ω n − 0 ) n − 1 ( ω n − 1 ) 0 ( ω n − 1 ) 1 ⋯ ( ω n − 1 ) n − 1 ⋮ ⋮ ⋱ ⋮ ( ω n − ( n − 1 ) ) 0 ( ω n − ( n − 1 ) ) 1 ⋯ ( ω n − ( n − 1 ) ) n − 1 ] [ A ( ω n 0 ) A ( ω n 1 ) ⋮ A ( ω n n − 1 ) ] \begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{n-1} \end{bmatrix} = \frac1n \begin{bmatrix} (\omega_n^{-0})^0 & (\omega_n^{-0})^1 & \cdots & (\omega_n^{-0})^{n-1} \\ (\omega_n^{-1})^0 & (\omega_n^{-1})^1 & \cdots & (\omega_n^{-1})^{n-1} \\ \vdots & \vdots & \ddots & \vdots \\ (\omega_n^{-(n-1)})^0 & (\omega_n^{-(n-1)})^1 & \cdots & (\omega_n^{-(n-1)})^{n-1} \end{bmatrix}\begin{bmatrix} A(\omega_n^0) \\ A(\omega_n^1) \\ \vdots \\ A(\omega_n^{n-1}) \end{bmatrix} ⎣⎢⎢⎢⎡a0a1⋮an−1⎦⎥⎥⎥⎤=n1⎣⎢⎢⎢⎡(ωn−0)0(ωn−1)0⋮(ωn−(n−1))0(ωn−0)1(ωn−1)1⋮(ωn−(n−1))1⋯⋯⋱⋯(ωn−0)n−1(ωn−1)n−1⋮(ωn−(n−1))n−1⎦⎥⎥⎥⎤⎣⎢⎢⎢⎡A(ωn0)A(ωn1)⋮A(ωnn−1)⎦⎥⎥⎥⎤
“这样,逆变换 就相当于把 正变换 过程中的 ω n k \omega_n^k ωnk 换成 ω n − k \omega_n^{-k} ωn−k,之后结果除以 n n n 就可以了。”——摘自某博客。
但是为什么我们不直接把
ω
n
k
\omega_n^k
ωnk 换成
1
n
×
ω
n
−
k
\frac1n\times \omega_n^{-k}
n1×ωn−k 算了呢?
实际上,因为
ω
n
−
k
=
ω
n
n
−
k
\omega_n^{-k}=\omega_n^{n-k}
ωn−k=ωnn−k ,也就是说它还是一个
n
n
n 次单位根。所以我们插值还是正常的该怎么插怎么插。如果换成
1
n
×
ω
n
−
k
\frac1n\times \omega_n^{-k}
n1×ωn−k 它就不是一个单位根,以上性质就不满足了。
伍、FFT 的代码实现
我们有两个版本——递归、迭代,相信大家都也想到了吧?
毋庸置疑的,递归版本确实很好写,将 F F T \tt FFT FFT 的思路全部放到上面即可,但是方便也有他的坏处——递归实现常数较大,这样对于一些题就会 T T T 飞,但是我还是写了,这里上代码了
const int MAXN=3e6;
const double Pi=acos(-1.0);
class task{
private:
struct cplx{
double vr,vi;//实部和虚部
cplx(const double R=0,const double I=0):vr(R),vi(I){}//构造函数
//------------------overload----------------//
cplx operator + (const cplx a)const{return cplx(vr+a.vr,vi+a.vi);}//重载加法
cplx operator - (const cplx a)const{return cplx(vr-a.vr,vi-a.vi);}
cplx operator * (const cplx a)const{return cplx(vr*a.vr-vi*a.vi,vr*a.vi+a.vr*vi);}
cplx operator / (const double var)const{return cplx(vr/var,vi/var);}
};
int n,m;
cplx a[MAXN+5],b[MAXN+5];
void fft(cplx* f,const int len,const short opt=1){
//opt==-1 : FFT 的逆变换
if(!len)return;
cplx f0[len+5],f1[len+5];
for(int i=0;i<len;++i)
f0[i]=f[i<<1],f1[i]=f[i<<1|1];
fft(f0,len>>1,opt);
fft(f1,len>>1,opt);
cplx w=cplx(cos(Pi/len),opt*sin(Pi/len)),buf=cplx(1,0);
for(int i=0;i<len;++i,buf=buf*w){
f[i]=f0[i]+buf*f1[i];
f[i+len]=f0[i]-buf*f1[i];
}
}
public:
inline void launch(){
qread(n,m);
rep(i,0,n)scanf("%lf",&a[i].vr);
rep(i,0,m)scanf("%lf",&b[i].vr);
for(m+=n,n=1;n<=m;n<<=1);
fft(a,n>>1);
fft(b,n>>1);
rep(i,0,n-1)a[i]=a[i]*b[i];
fft(a,n>>1,-1);
rep(i,0,m)writc((int)((a[i].vr)/n+0.5),' ');
Endl;
}
}This;
signed main(){
#ifdef FILEOI
freopen("file.in","r",stdin);
freopen("file.out","w",stdout);
#endif
This.launch();
return 0;
}
似乎递归版本比较好写,现在我们来看一下迭代(递推)版本应该怎么做:
原序列: 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 , 4 , 2 , 6 , 1 , 5 , 3 , 7 0,4,2,6,1,5,3,7 0,4,2,6,1,5,3,7
转换为二进制再来看看。
原序列: 000 , 001 , 010 , 011 , 100 , 101 , 110 , 111 000,001,010,011,100,101,110,111 000,001,010,011,100,101,110,111
终序列: 000 , 100 , 010 , 110 , 001 , 101 , 011 , 111 000,100,010,110,001,101,011,111 000,100,010,110,001,101,011,111
可以发现终序列是原序列每个元素的二进制翻转。
于是我们可以先把要变换的系数排在相邻位置,从下往上迭代。
这个二进制翻转过程可以自己脑补方法,只要保证时间复杂度 O ( n log n ) O(n\log n) O(nlogn) ,代码简洁就可以了。
在这里给出一个参考的方法:
我们对于每个 i i i ,假设已知 i − 1 i-1 i−1 的翻转为 j j j 。考虑不进行翻转的二进制加法怎么进行:从最低位开始,找到第一个为 0 0 0 的二进制位,将它之前的 1 1 1 变为 0 0 0 ,将它自己变为 1 1 1 。因此我们可以从 j j j 的最高位开始,倒过来进行这个过程。
这是迭代版本:
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
#define rep(i,__l,__r) for(register int i=__l,i##_end_=__r;i<=i##_end_;++i)
#define fep(i,__l,__r) for(register int i=__l,i##_end_=__r;i>=i##_end_;--i)
#define writc(a,b) fwrit(a),putchar(b)
#define mp(a,b) make_pair(a,b)
#define ft first
#define sd second
#define LL long long
#define ull unsigned long long
#define pii pair<int,int>
#define Endl putchar('\n')
// #define FILEOI
// #define int long long
#ifdef FILEOI
#define MAXBUFFERSIZE 500000
inline char fgetc(){
static char buf[MAXBUFFERSIZE+5],*p1=buf,*p2=buf;
return p1==p2&&(p2=(p1=buf)+fread(buf,1,MAXBUFFERSIZE,stdin),p1==p2)?EOF:*p1++;
}
#undef MAXBUFFERSIZE
#define cg (c=fgetc())
#else
#define cg (c=getchar())
#endif
template<class T>inline void qread(T& x){
char c;bool f=0;
while(cg<'0'||'9'<c)f|=(c=='-');
for(x=(c^48);'0'<=cg&&c<='9';x=(x<<1)+(x<<3)+(c^48));
if(f)x=-x;
}
inline int qread(){
int x=0;char c;bool f=0;
while(cg<'0'||'9'<c)f|=(c=='-');
for(x=(c^48);'0'<=cg&&c<='9';x=(x<<1)+(x<<3)+(c^48));
return f?-x:x;
}
template<class T,class... Args>inline void qread(T& x,Args&... args){qread(x),qread(args...);}
template<class T>inline T Max(const T x,const T y){return x>y?x:y;}
template<class T>inline T Min(const T x,const T y){return x<y?x:y;}
template<class T>inline T fab(const T x){return x>0?x:-x;}
inline int gcd(const int a,const int b){return b?gcd(b,a%b):a;}
inline void getInv(int inv[],const int lim,const int MOD){
inv[0]=inv[1]=1;for(int i=2;i<=lim;++i)inv[i]=1ll*inv[MOD%i]*(MOD-MOD/i)%MOD;
}
template<class T>void fwrit(const T x){
if(x<0)return (void)(putchar('-'),fwrit(-x));
if(x>9)fwrit(x/10);putchar(x%10^48);
}
inline LL mulMod(const LL a,const LL b,const LL mod){//long long multiplie_mod
return ((a*b-(LL)((long double)a/mod*b+1e-8)*mod)%mod+mod)%mod;
}
const int MAXN=3e6;
const double Pi=acos(-1.0);
class task{
private:
struct cplx{
double vr,vi;//实部和虚部
cplx(const double R=0,const double I=0):vr(R),vi(I){}//构造函数
//------------------overload----------------//
cplx operator + (const cplx a)const{return cplx(vr+a.vr,vi+a.vi);}//重载加法
cplx operator - (const cplx a)const{return cplx(vr-a.vr,vi-a.vi);}
cplx operator * (const cplx a)const{return cplx(vr*a.vr-vi*a.vi,vr*a.vi+a.vr*vi);}
cplx operator / (const double var)const{return cplx(vr/var,vi/var);}
};
int n,m;
cplx a[MAXN+5],b[MAXN+5];
int revi[MAXN+5];
/*
f(w^x)
=f0(w^{2x})+w^x*f1(w^{2x})
=a0+a2+a4+...,a1+a3+a5+...
=a0+a4+a8+...+a2+a6+a10...+a1+a5+a9+...+a3+a7+a11...
=f00(w^{4x})+w^{2x}*f01(w^{4x})+w^x*f10(w^{4x})+w^3x*f11(w^{4x})
=a0+a4+a8+...,w^{2x}*(a2+a6+a10...),w^x*(a1+a5+a9+...),w^{3x}*(a3+a7+a11...)
f_s -> 下标将 s 反过来之后, a_i 的 i 的二进制反过来与 s 相同
.
.
.
s
000 001 010 011
|反过来
v
000 100 010 110
a0 a{k/2} a{3*k/4} a{k}
a0 a4 a2 a6
*/
/*
void fft(cplx* f,const int len,const short opt=1){
if(!len)return;
cplx f0[len+5],f1[len+5];
for(int i=0;i<len;++i)
f0[i]=f[i<<1],f1[i]=f[i<<1|1];
fft(f0,len>>1,opt);
fft(f1,len>>1,opt);
cplx w=cplx(cos(Pi/len),opt*sin(Pi/len)),buf=cplx(1,0);
for(int i=0;i<len;++i,buf=buf*w){
f[i]=f0[i]+buf*f1[i];
f[i+len]=f0[i]-buf*f1[i];
}
}
*/
inline void fft(cplx* f,const short opt=1){
for(int i=0;i<n;++i)if(i<revi[i])
swap(f[i],f[revi[i]]);
for(int p=2;p<=n;p<<=1){
//枚举层数
int len=p/2;//上一层的一半的长度
cplx tmp(cos(Pi/len),opt*sin(Pi/len));
//单位复根
for(int k=0;k<n;k+=p){
cplx buf(1,0);//记录 omega 的次方
for(int l=k;l<k+len;++l,buf=buf*tmp){
//每次 buf 累成单位 omega
cplx tt=buf*f[len+l];
f[len+l]=f[l]-tt;
f[l]=f[l]+tt;
/*
此处与递归版本的 FFT 中这一段是一样的:
f[i]=f0[i]+buf*f1[i];
f[i+len]=f0[i]-buf*f1[i];
*/
}
}
}
if(opt==-1)for(int i=0;i<n;++i)f[i]=f[i]/n;
//如果是 逆变换 , 那么需要全部 /n
}
public:
inline void launch(){
qread(n,m);
rep(i,0,n)scanf("%lf",&a[i].vr);
rep(i,0,m)scanf("%lf",&b[i].vr);
for(m+=n,n=1;n<=m;n<<=1);
rep(i,0,n-1)revi[i]=(revi[i>>1]>>1)|((i&1)?n>>1:0);//处理反转
fft(a);fft(b);
rep(i,0,n-1)a[i]=a[i]*b[i];
fft(a,-1);
rep(i,0,m)writc((int)(a[i].vr+0.5),' ');
Endl;
}
}This;
signed main(){
#ifdef FILEOI
freopen("file.in","r",stdin);
freopen("file.out","w",stdout);
#endif
This.launch();
return 0;
}
如果我可以回到过去,我一定会去当杀手。
为什么?因为我要去干翻欧某和傅某某…