文章目录
- 前置知识:复数
- 引子:虚数
- 定义
- 计算
- 性质
- 有关多项式
- 点值多项式相乘
- 大整数乘法
- FFT \textit{FFT} FFT
- 离散傅里叶变换
- 快速傅里叶变换
- 代码实现
- 蝴蝶变换
- 计算 ω n − x \omega_n^{-x} ωn−x
- 代码壹号
- 改进方案
- 精度提升
- 常数优化:二合一
- 常数优化:不蝴蝶变换
- 提醒:如果混用
- 再探 FFT \textit{FFT} FFT
- NTT \textit{NTT} NTT
- 引子:原根
- 模仿 FFT \textit{FFT} FFT
- 改进:任意模数
- CNTT \textit{CNTT} CNTT
- 64-bit \text{64-bit} 64-bit 整数的卷积
我最初学习 FFT \textit{FFT} FFT 的资料是《小学生都能看懂的 FFT \textit{FFT} FFT》。这当然可以助你快速起步。但说实在话,数学是很重要(也很有趣)的,你应该把复数当做需要掌握的知识点(而不是 FFT \textit{FFT} FFT 中的引理)。
还有 OI-wiki \text{OI-wiki} OI-wiki 应该也能提供不少帮助。我当年完全不知道有这玩意儿。
前置知识:复数
引子:虚数
定义 i i i 为使得 i 2 = − 1 i^2=-1 i2=−1 成立的值。一般我们把 k i ( k ∈ R ) ki\;(k\in\R) ki(k∈R) 叫做 虚数。
这个 i i i 不是实数,因此也无法写成 0.142857 0.142857 0.142857 或 cos ( Φ ) \cos(\Phi) cos(Φ) 等等。我们只能保留这个 i i i 。这就是一种记法而已。
不难发现,此时任意 x < 0 x<0 x<0 都可以定义 x = i − x \sqrt{x}=i\sqrt{-x} x=i−x,因此 i i i 使得所有数的平方根都是有定义的。甚至 i i i 也有平方根,这就会涉及到马上要讲的 复数
。
如果你对域有所了解:这其实就是 C = R ( i ) \Complex=\Reals(i) C=R(i) 括域的结果。然而了解域之前应该会先学复数吧。
定义
形如 z = x + y i z=x+yi z=x+yi 的数字,我们称之为 复数。其 实部 为 x x x 而 虚部 为 y y y 。
我们可以用一个直角坐标系中的点 ( x , y ) (x,y) (x,y) 表示 z = x + y i z=x+yi z=x+yi 。发现和数轴类似,是一一对应关系。
由于是直角坐标系,也可以用幅角和模长的形式描述,即 z = r ⋅ ( cos θ + i sin θ ) z=r\cdot(\cos\theta+i\sin\theta) z=r⋅(cosθ+isinθ),其中 θ \theta θ 是极角、 r r r 是长度(即 r = x 2 + y 2 r=\sqrt{x^2+y^2} r=x2+y2,为向量的模长)。
著名的欧拉公式 e x i = cos x + i sin x e^{xi}=\cos x+i\sin x exi=cosx+isinx 则为我们提供了另一个表示法: z = e θ i r z=e^{\theta i}r z=eθir 。我觉得欧拉公式是应该被了解的。
计算
那么 z 1 z 2 z_1z_2 z1z2 等于多少呢?设 z 1 = r 1 ⋅ ( cos θ 1 + i sin θ 1 ) , z 2 = r 2 ⋅ ( cos θ 2 + i sin θ 2 ) z_1=r_1\cdot(\cos\theta_1+i\sin\theta_1),\;z_2=r_2\cdot(\cos\theta_2+i\sin\theta_2) z1=r1⋅(cosθ1+isinθ1),z2=r2⋅(cosθ2+isinθ2),那么
z 1 z 2 = r 1 r 2 ⋅ [ ( cos θ 1 cos θ 1 − sin θ 1 sin θ 1 ) + i ( sin θ 1 cos θ 2 + cos θ 1 sin θ 2 ) ] = r 1 r 2 ⋅ [ cos ( θ 1 + θ 2 ) + i sin ( θ 1 + θ 2 ) ] \begin{align*} z_1 z_2 &=r_1 r_2\cdot[(\cos\theta_1\cos\theta_1-\sin\theta_1\sin\theta_1)+i(\sin\theta_1\cos\theta_2+\cos\theta_1\sin\theta_2)]\\ &=r_1 r_2\cdot[\cos(\theta_1+\theta_2)+i\sin(\theta_1+\theta_2)] \end{align*} z1z2=r1r2⋅[(cosθ1cosθ1−sinθ1sinθ1)+i(sinθ1cosθ2+cosθ1sinθ2)]=r1r2⋅[cos(θ1+θ2)+isin(θ1+θ2)]
这告诉我们一个口诀:模长相乘,幅角相加。
当然,用欧拉公式表示法甚至用不到和差角公式,可以直接证明。
性质
首先,由于 幅角相加
,所以 x n = 1 x^n={1} xn=1 有 n n n 个取值。恰好把 2 π 2\pi 2π 的周角给 n n n 等分。
上图为 n = 8 n=8 n=8 的情况。
用 ω n \omega_n ωn 表示所有 x x x 的解中,非零幅角最小的一个。也就是幅角为 2 π n 2\pi\over n n2π、模长为 1 1 1 的复数。这个 ω n \omega_n ωn 被称为 “单位复根”。
明显有这几个性质:
- 可以同时扩倍,因为本质是比例关系。即
ω n k = ω n / 2 k / 2 \omega_n^k=\omega_{n/2}^{k/2} ωnk=ωn/2k/2
- 在环上,对面同样也有一个点。即 n n n 为偶数时
ω n k = − ω n k + n 2 \omega_n^k=-\omega_n^{k+\frac{n}{2}} ωnk=−ωnk+2n
- 转一圈就会回家。根据定义
ω n n = 1 \omega_n^n=1 ωnn=1
有关多项式
如果已知 n n n 次多项式经过的 ( n + 1 ) (n{+}1) (n+1) 个点,我们就可以建立方程,唯一确定这个多项式。可参考 拉格朗日插值
。
为什么方程是线性无关的呢?如果你知道 f ( x 0 ) = f ( x ) m o d ( x − x 0 ) f(x_0)=f(x)\bmod(x{-}x_0) f(x0)=f(x)mod(x−x0),并且你知道多项式是可以定义 gcd \gcd gcd 的(也就是欧几里得环),那么 gcd ( x − x 1 , x − x 2 ) = 1 ( x 1 ≠ x 2 ) \gcd(x{-}x_1,x{-}x_2)=1\;(x_1\ne x_2) gcd(x−x1,x−x2)=1(x1=x2) 说明模数两两互质,根据 CRT \textit{CRT} CRT 我们可以还原解。
点值多项式相乘
假设 f ( x ) f(x) f(x) 经过 ( x 0 , a ) (x_0,a) (x0,a),而 g ( x ) g(x) g(x) 经过 ( x 0 , b ) (x_0,b) (x0,b),那么考虑 h ( x ) = f ( x ) g ( x ) h(x)=f(x)g(x) h(x)=f(x)g(x),发现 h ( x 0 ) = f ( x 0 ) g ( x 0 ) = a b h(x_0)=f(x_0)g(x_0)=ab h(x0)=f(x0)g(x0)=ab,也就是说, h ( x ) h(x) h(x) 经过 ( x 0 , a b ) (x_0,ab) (x0,ab) 。
所以 点值多项式相乘是 O ( n ) \mathcal O(n) O(n) 的。在一些情况下,这启发我们直接维护若干点值(完全忘却多项式的系数表示法)。
但是 FFT \textit{FFT} FFT 能够使得任意 n n n 次多项式都能在 O ( n log n ) \mathcal O(n\log n) O(nlogn) 的时间内完成点值与系数表示的转化,因此获得了最快的多项式乘法方案。
大整数乘法
将各位数字视为系数,最后求 f ( 10 ) f(10) f(10) 即可。
或者你可以这样说:本质都是卷积。
FFT \textit{FFT} FFT
全称是 快速傅里叶变换( Fast Fourier Transform \text{Fast Fourier Transform} Fast Fourier Transform),高斯在 1805 1805 1805 年(电脑问世之前)就发明了它。计算数学真恐怖。
但为什么叫傅里叶变换呢?这涉及到傅里叶级数
g ( t ) ∼ ∑ n = − ∞ + ∞ f n exp ( 2 π i n t ) f n : = ∫ − ∞ + ∞ exp ( − 2 π i n t ) g ( t ) d t g(t)\sim\sum_{n=-\infty}^{+\infty}f_n\exp(2\pi int)\\ f_n:=\int_{-\infty}^{+\infty}\exp(-2\pi int)g(t)\mathrm{d}t g(t)∼n=−∞∑+∞fnexp(2πint)fn:=∫−∞+∞exp(−2πint)g(t)dt
这或许是看 3b1b \text{3b1b} 3b1b 的绝佳借口。尽管其事实上并不干涉我们理解傅里叶变换。
因此我们对 k n ( 0 ⩽ k < n ) \frac{k}{n}\;(0\leqslant k<n) nk(0⩽k<n) 做频率,提取傅里叶级数的系数,就得到离散傅里叶变换( Discrete Fourier Transform \text{Discrete Fourier Transform} Discrete Fourier Transform)。
离散傅里叶变换
通过计算 f ( ω n k ) ( 0 ⩽ k < n ) f(\omega_n^{\thinspace k})\;(0\leqslant k<n) f(ωnk)(0⩽k<n),我们得到了 n n n 个点值。这就是 DFT \textit{DFT} DFT 正变换了。
我们可以把它轻易地变换回原系数。只需要以 f ( ω n k ) f(\omega_n^{\thinspace k}) f(ωnk) 为 x k x^k xk 的系数(得到 n n n 次多项式)然后求 ω n − k \omega_n^{-k} ωn−k 的点值得到
∑ j = 0 n − 1 f ( ω n j ) ( ω n − k ) j = ∑ j = 0 n − 1 ∑ t = 0 n − 1 a t ω n j t ( ω n − k ) j = ∑ t = 0 n − 1 a t ∑ j = 0 n − 1 ω n ( k − t ) j \begin{align*} \sum_{j=0}^{n-1}f(\omega_n^{\thinspace j})(\omega_n^{-k})^j &=\sum_{j=0}^{n-1}\sum_{t=0}^{n-1}a_t\omega_n^{\thinspace jt}(\omega_n^{-k})^j\\ &=\sum_{t=0}^{n-1}a_t\sum_{j=0}^{n-1}\omega_n^{(k-t)j} \end{align*} j=0∑n−1f(ωnj)(ωn−k)j=j=0∑n−1t=0∑n−1atωnjt(ωn−k)j=t=0∑n−1atj=0∑n−1ωn(k−t)j
后面那个关于 j j j 的求和,是等比数列求和。当 k = t k=t k=t 时公比为 1 1 1,显然和为 n n n 。若 k ≠ t k\ne t k=t,则该值为
ω n ( k − t ) n − ω n 0 ω n k − t − 1 = 1 − 1 ω n k − t − 1 = 0 {\omega_n^{(k-t)n}-\omega_n^{\thinspace 0}\over\omega_{n}^{k-t}-1}=\frac{1-1}{\omega_n^{k-t}-1}=0 ωnk−t−1ωn(k−t)n−ωn0=ωnk−t−11−1=0
可以想到,这就是对 ∫ 0 1 exp ( 2 π i n t ) d t = 0 \int_{0}^{1}\exp(2\pi int)\text dt=0 ∫01exp(2πint)dt=0 的离散模拟(这个叫法不严谨)。
因此,通过第二次插值,我们就可以得到原系数了。这就是逆变换。
完整地重复一遍结论:设 f ( x ) = ∑ j = 0 n − 1 a j x j f(x)=\sum_{j=0}^{n-1}a_jx^j f(x)=∑j=0n−1ajxj,记 g ( x ) = ∑ j = 0 n − 1 f ( ω n j ) x j g(x)=\sum_{j=0}^{n-1}f(\omega_n^{\thinspace j})x^j g(x)=∑j=0n−1f(ωnj)xj,则
a k = n − 1 g ( ω n − k ) a_k=n^{-1}g(\omega_n^{-k}) ak=n−1g(ωn−k)
快速傅里叶变换
设
f ( x ) = ∑ i = 0 n − 1 a i x i f(x)=\sum_{i=0}^{n-1}a_ix^i f(x)=i=0∑n−1aixi
其中 n n n 为偶数。令
f 1 ( x ) = a 0 + a 2 x + a 4 x 2 + ⋯ + a n − 2 x n 2 − 1 f 2 ( x ) = a 1 + a 3 x + a 5 x 2 + ⋯ + a n − 1 x n 2 − 1 f_1(x)=a_0+a_2x+a_4x^2+\cdots+a_{n-2}x^{\frac{n}{2}-1}\\ f_2(x)=a_1+a_3x+a_5x^2+\cdots+a_{n-1}x^{\frac{n}{2}-1} f1(x)=a0+a2x+a4x2+⋯+an−2x2n−1f2(x)=a1+a3x+a5x2+⋯+an−1x2n−1
那么
f ( x ) = f 1 ( x 2 ) + x f 2 ( x 2 ) f(x)=f_1(x^2)+xf_2(x^2) f(x)=f1(x2)+xf2(x2)
将 x j = ω n j x_j=\omega_n^{\thinspace j} xj=ωnj 代入其中:
f ( ω n j ) = f 1 ( ω n 2 j ) + ω n j ⋅ f 2 ( ω n 2 j ) = f 1 ( ω n / 2 j ) + ω n j ⋅ f 2 ( ω n / 2 j ) \begin{align*} f(\omega_n^j) &=f_1(\omega_n^{2j})+\omega_n^{\thinspace j}\cdot f_2(\omega_n^{2j})\\ &=f_1(\omega_{n/2}^{\thinspace j})+\omega_n^{\thinspace j}\cdot f_2(\omega_{n/2}^{\thinspace j}) \end{align*} f(ωnj)=f1(ωn2j)+ωnj⋅f2(ωn2j)=f1(ωn/2j)+ωnj⋅f2(ωn/2j)
同理可得
f ( ω n j + n / 2 ) = f 1 ( ω n 2 j + n ) + ω n j + n / 2 ⋅ f 2 ( ω n 2 j + n ) = f 1 ( ω n / 2 j ) − ω n j ⋅ f 2 ( ω n / 2 j ) \begin{aligned} f(\omega_n^{j+n/2})&=f_1(\omega_n^{2j+n})+\omega_n^{j+n/2}\cdot f_2(\omega_n^{2j+n})\\ &=f_1(\omega_{n/2}^{\thinspace j})-\omega_n^{\thinspace j}\cdot f_2(\omega_{n/2}^{\thinspace j}) \end{aligned} f(ωnj+n/2)=f1(ωn2j+n)+ωnj+n/2⋅f2(ωn2j+n)=f1(ωn/2j)−ωnj⋅f2(ωn/2j)
所以只需求解 f 1 , f 2 f_1,f_2 f1,f2 的 ω n / 2 j \omega_{n/2}^{\thinspace j} ωn/2j 处点值。这可以递归。复杂度 T ( n ) = O ( n ) + 2 T ( n 2 ) = O ( n log n ) T(n)=\mathcal O(n)+2T({n\over 2})=\mathcal O(n\log n) T(n)=O(n)+2T(2n)=O(nlogn) 。
代码实现
蝴蝶变换
递归是耗费颇多的。我们研究一下划分系数的规律,以此去掉递归。
第一次划分,如果 x x x 是奇数,那么就会 + n 2 +\frac{n}{2} +2n(分到右边)。然后 x x x 成为了它所在那半边的第 ⌊ x 2 ⌋ \lfloor{x\over 2}\rfloor ⌊2x⌋ 个系数。
对于第二次划分,如果 ⌊ x 2 ⌋ \lfloor{\frac{x}{2}}\rfloor ⌊2x⌋ 是奇数,那么就会 + n 4 +\frac{n}{4} +4n(分到右边)。然后 x x x 成为了 ⌊ x 4 ⌋ \lfloor{x\over 4}\rfloor ⌊4x⌋ 。
一直到第 i i i 次划分,如果 ⌊ x 2 i − 1 ⌋ \lfloor{x\over 2^{i-1}}\rfloor ⌊2i−1x⌋ 是奇数,也就是二进制下第 i i i 位(从 1 1 1 开始编号)为 1 1 1,则走到的位置的二进制表示下第 ( n + 1 − i ) (n+1-i) (n+1−i) 位为 1 1 1 。
所以 将原下标二进制表示翻转,就是新位置的二进制表示。注意到移位先于所有计算,因此可以先移位,然后自底向上逐层模拟递归回溯前的计算。
计算 ω n − x \omega_n^{-x} ωn−x
由于 ω n − x = ω n n − x \omega_n^{-x}=\omega_n^{n-x} ωn−x=ωnn−x,那么我们先求出 ω n x \omega_{n}^{\thinspace x} ωnx 作为自变量的点值。最后需要的点值 b x b_x bx 对应的自变量是 ω n − x = ω n n − x \omega_{n}^{-x}=\omega_{n}^{n-x} ωn−x=ωnn−x,也就是说第 x x x 项应该放在第 ( n − x ) (n{-}x) (n−x) 项处;直接将得到的点值数组的第 1 1 1 到 ( n − 1 ) (n{-}1) (n−1) 位翻转即可。
注意:如果只用 sin , cos \sin,\cos sin,cos 或者 exp \exp exp 计算 ω n 1 \omega_n^{\thinspace 1} ωn1 再自乘,速度较快;对于每一个都用三角函数求解,速度较慢,但是精度更高(事实上前者的精度损失也并不是很大)。
当然,因为我们已经把它预处理出来了,所以不需要上面所说的 r e v e r s e \tt reverse reverse 的技巧;直接用 ω n n − x \omega_{n}^{n-x} ωnn−x 代入,可以省去翻转的过程。
代码壹号
解释一下:我们预处理了幅角为 2 π n \frac{2\pi}{n} n2π 的单位复根的幂,现在对于长度为 2 w 2w 2w 的区间,相当于需要求出幅角为 2 π 2 w \frac{2\pi}{2w} 2w2π 的单位复根。显然它就是 2 π n ⋅ n 2 w \frac{2\pi}{n}\cdot\frac{n}{2w} n2π⋅2wn,也就是预处理数组中的第 n 2 w n\over 2w 2wn 位。
另外,这个代码不必背下来。因为后面会讲更好的写法。
using cplx = complex<double>; // in file <complex>
const double pi = acos(-1);
cplx omg[MAXN]; int went[MAXN];
/** @param opt 1 to DFT, -1 to IDFT */
void FFT(cplx a[], int n, int opt){const int N = 1<<n; // real lengthfor(int i=1; i!=N; ++i){went[i] = (went[i>>1]>>1)|((i&1)<<n>>1);if(i < went[i]) swap(a[i],a[went[i]]);}for(int i=1; i!=N; ++i){omg[i].real(cos(2*pi*i/N));omg[i].imag(sin(2*pi*i/N));}omg[0].imag(0), omg[0].real(1);for(int w=1; w!=N; w<<=1)for(cplx *p=a; p!=a+N; p+=(w<<1))for(int i=0; i<w; ++i){cplx t = omg[(N/(w<<1)*i*opt+N)%N];t = t*p[i+w], p[i+w] = p[i]-t, p[i] += t;}if(!(~opt)) for(int i=0; i!=N; ++i) a[i] /= N;
}
改进方案
精度提升
众所周知, d o u b l e \tt double double 的能力有限。哪怕是 l o n g d o u b l e \tt long\; double longdouble,也有力不从心的时候。问题在于 数值太大,而过程中不能对值进行任何操作。尤其是对 ( 1 0 9 + 7 ) (10^9{+}7) (109+7) 等大质数取模时,中途是不能取模的——毕竟涉及实数乘法。
如何防爆呢?核心肯定在于,减小结果的大小。要么减少数字个数,要么减小数字大小。对于第一个,考虑 K a r a t s u b a \tt Karatsuba Karatsuba 乘法,每次让数字个数变为 1 2 1\over 2 21,只能让结果变为 1 4 1\over 4 41 。好像没有更好的方法了。此路不通。
那么怎么将数字减小呢?显然减法不行,考虑除法。实数除法也不行——数字变小了,但是小数点后的位数变多了,仍然存在精度问题。看来只有一条路了——整数除法,即整除。对某一个数作除法,照样无益;必须对所有数都作相同的除法。
比如除数是 M M M 。那么一个数变为 ⌊ a i M ⌋ M + ( a i m o d M ) \lfloor{a_i\over M}\rfloor M+(a_i\bmod M) ⌊Mai⌋M+(aimodM),为了方便,记为 v i M + r i v_iM+r_i viM+ri 。于是 a i b j = r i b j + ( v i b j ) M a_ib_j=r_ib_j+(v_ib_j)M aibj=ribj+(vibj)M 。发现这两项的贡献是独立的,可以单独计算,最后相加!形式化地,写出两个新的多项式
f l o w ( x ) = ∑ i = 0 n − 1 ( a i m o d M ) x i f h i g h ( x ) = ∑ i = 0 n − 1 ⌊ a i M ⌋ x i f_{low}(x)=\sum_{i=0}^{n-1}\left(a_i\bmod M\right)x^i\\ f_{high}(x)=\sum_{i=0}^{n-1}\left\lfloor\frac{a_i}{M}\right\rfloor x^i flow(x)=i=0∑n−1(aimodM)xifhigh(x)=i=0∑n−1⌊Mai⌋xi
则 f ( x ) g ( x ) = f l o w ( x ) g ( x ) + M ⋅ f h i g h ( x ) g ( x ) f(x)g(x)=f_{low}(x)g(x)+M\cdot f_{high}(x)g(x) f(x)g(x)=flow(x)g(x)+M⋅fhigh(x)g(x) 。显然取 M = max a i M=\sqrt{\max a_i} M=maxai 能够使得值下降的最快。如果对 g ( x ) g(x) g(x) 再做一次此等拆分,则每个值的结果都在 ( max a i ) ⋅ n (\max a_i)\cdot n (maxai)⋅n 以内,效果拔群!
另外说一句:一般都会对 f ( x ) , g ( x ) f(x),g(x) f(x),g(x) 同时作拆分,因为这样只需要 4 4 4 次正变换、 3 3 3 次逆变换;如果封装过度,每次都调用 multiplies
(将会调用 2 2 2 次正变换、 1 1 1 次逆变换)就会变为 8 8 8 次正变换、 4 4 4 次逆变换,大大的不好!
常数优化:二合一
如果我们要计算两个 系数为实数(虽然也基本上不可能是复数)的多项式 A ( x ) , B ( x ) A(x),B(x) A(x),B(x) 的乘积,似乎要做两次正变换?
可以这样:令 P ( x ) = A ( x ) + i B ( x ) , Q ( x ) = A ( x ) − i B ( x ) P(x)=A(x)+iB(x),\;Q(x)=A(x)-iB(x) P(x)=A(x)+iB(x),Q(x)=A(x)−iB(x),则只需要求出这两个的点值,相加除以二就是 A A A 的点值,相减除以 2 i 2i 2i 就是 B B B 的点值。
而二者的共轭关系,使得我们有 “二合一” 的方案。用 z ‾ \overline{z} z 表示 z z z 的共轭。
P ( ω n k ) = ∑ j = 0 n ( a j + i b j ) ⋅ ω n k j = ∑ j = 0 n ( a j − i b j ) ω n ( n − k ) j ‾ = Q ( ω n n − k ) ‾ P(\omega_n^k)=\sum_{j=0}^{n}(a_j+ib_j)\cdot \omega_{n}^{kj}\\ =\sum_{j=0}^{n}\overline{(a_j-ib_j)\omega_{n}^{(n-k)j}}=\overline{Q(\omega_{n}^{n-k})} P(ωnk)=j=0∑n(aj+ibj)⋅ωnkj=j=0∑n(aj−ibj)ωn(n−k)j=Q(ωnn−k)
当然你觉得这个很离谱,怎么就能够 “二合一” 呢?事实上,复数就是二元组,你相当于把两个数组放在一起变换罢了。所以你其实也可以用复数实现 “二合一” 的任意其他变换,但是效率可能并无提升,因为复数加法相较于实数加法就已经是两倍常数了。只是 FFT \textit{FFT} FFT 中,本来就是复数运算,所以是真正的常数优化。
注意上面的过程中,假定了 ( a j + i b j ) (a_j+ib_j) (aj+ibj) 的共轭是 ( a j − i b j ) (a_j-ib_j) (aj−ibj),所以必须要 a j , b j ∈ R a_j,b_j\in\R aj,bj∈R 。
逆变换也可以 “二合一”,只要 原多项式系数是实数。假设我们已经知道 A ( ω j ) , B ( ω j ) A(\omega^j),B(\omega^j) A(ωj),B(ωj) 两个点值序列,则假设存在系数为复数的多项式 P ( x ) P(x) P(x) 使得其点值为
P ( ω j ) = A ( ω j ) + i B ( ω j ) P(\omega^j)=A(\omega^j)+iB(\omega^j) P(ωj)=A(ωj)+iB(ωj)
逆变换后,得到一个复系数多项式 P ( x ) P(x) P(x) 。若将 P ( x ) P(x) P(x) 的系数按照实部与虚部拆分,即 P ( x ) = F ( x ) + i ⋅ G ( x ) P(x)=F(x)+i\cdot G(x) P(x)=F(x)+i⋅G(x),其中 F ( x ) , G ( x ) F(x),G(x) F(x),G(x) 系数均为实数,那么显然有 P ( ω j ) = F ( ω j ) + i G ( ω j ) P(\omega^j)=F(\omega^j)+iG(\omega^j) P(ωj)=F(ωj)+iG(ωj) 。
由于 A ( x ) , B ( x ) A(x),B(x) A(x),B(x) 系数均为实数,立刻可得 { F ( x ) = A ( x ) G ( x ) = B ( x ) \begin{cases}F(x)=A(x)\\G(x)=B(x)\end{cases} {F(x)=A(x)G(x)=B(x) 是一组解。而 F ( x ) , G ( x ) F(x),G(x) F(x),G(x) 是唯一的(因为它们代表着 P ( x ) P(x) P(x) 的系数拆分),所以这就是唯一解。所以 P ( x ) P(x) P(x) 的系数的实部是 A ( x ) A(x) A(x),而虚部是 B ( x ) B(x) B(x),搞定!
常数优化:不蝴蝶变换
求点值的本质是乘了一个矩阵
( ω n 0 × 0 ω n 1 × 0 ⋯ ω n n × 0 ω n 0 × 1 ω n 1 × 1 ⋯ ω n n × 1 ⋮ ⋮ ⋱ ⋮ ω n 0 × n ω n 1 × n ⋯ ω n n × n ) \begin{pmatrix} \omega_{n}^{0\times 0} & \omega_{n}^{1\times 0} & \cdots & \omega_{n}^{n\times 0}\\ \omega_{n}^{0\times 1} & \omega_{n}^{1\times 1} & \cdots & \omega_{n}^{n\times 1}\\ \vdots & \vdots & \ddots & \vdots\\ \omega_{n}^{0\times n} & \omega_{n}^{1\times n} & \cdots & \omega_{n}^{n\times n}\\ \end{pmatrix} ⎝ ⎛ωn0×0ωn0×1⋮ωn0×nωn1×0ωn1×1⋮ωn1×n⋯⋯⋱⋯ωnn×0ωnn×1⋮ωnn×n⎠ ⎞
而 FFT \textit{FFT} FFT 将其拆解为两个线性变换:将 a i a_i ai 放到二进制位翻转后的 i i i 的位置,称为蝴蝶变换;将蝴蝶变换后的序列进行各种操作,我们姑且称之为 “傅外叶变换” 吧。
考虑将上面的矩阵 转置。由于上面的矩阵是沿对角线对称的,所以转置不带来任何变化。但是对应到代码中,我们可以先进行 “傅外叶变换” 的转置,再进行蝴蝶变换的转置。而蝴蝶变换是交换位置,是沿对角线对称的,也没有区别。
做完这样的正变换之后,再做傅里叶变换(普通版)就可以把序列变回去。此时我们会发现:做傅里叶变换前需要一次蝴蝶变换,而做完 “傅外叶变换” 后也有一次蝴蝶变换。这不就相当于啥也没做嘛!
于是蝴蝶变换被抛弃了。蝴蝶变换是 random-access \text{random-access} random-access,常数较大,去掉它是一个不错的优化!
最后提一句,关于矩阵转置。傅里叶变换是自底向上,行向量 ( p i , p i + L ) (p_i,p_{i+L}) (pi,pi+L) 乘 ( 1 1 ω i − ω i ) \begin{pmatrix}1 & 1 \\ \omega^{i} & -\omega^i\end{pmatrix} (1ωi1−ωi),所以 “傅外叶变换” 是行向量 ( p i , p i + L ) (p_i,p_{i+L}) (pi,pi+L) 乘 ( 1 ω i 1 − ω i ) \begin{pmatrix} 1 & \omega^i \\ 1 & -\omega^i\end{pmatrix} (11ωi−ωi),自顶向下进行。
typedef complex<double> fushu;
fushu omg[MAXN<<1];
void prepare(int n){for(int i=0; i<n; ++i){omg[i].imag(sin(2*M_PI*i/n));omg[i].real(cos(2*M_PI*i/n));}
}
void FFT(fushu a[],int n){const fushu *end_a = a+n;for(int w=n>>1,x=1; w; w>>=1,x<<=1)for(fushu *p=a; p!=end_a; p+=(w<<1))for(int i=0; i<w; ++i){const fushu l = p[i], r = p[i+w];p[i] = l+r, p[i+w] = (l-r)*omg[x*i];}
}
void DFFT(fushu a[],int n){const fushu *end_a = a+n;for(int w=1,x=n>>1; x; w<<=1,x>>=1)for(fushu *p=a; p!=end_a; p+=(w<<1))for(int i=0; i<w; ++i){fushu t = p[i+w]*omg[x*i];p[i+w] = p[i]-t, p[i] += t;}std::reverse(a+1,a+n);for(int i=0; i<n; ++i) a[i] /= n;
}
提醒:如果混用
本来我这里写的是 “不能混用”,然后被 Q u a c k \sf Quack Quack 光速打脸了……握巢,这摸猛犸?
能不能混用 不做蝴蝶变换
和 二合一
呢?可以,但是稍微麻烦一点点。注意到 不做蝴蝶变换
等价于多做了一次蝴蝶变换,所以此时 ω n x \omega_n^x ωnx 对应的点值实际上放在 rev ( x ) \text{rev}(x) rev(x) 的位置。而 二合一
需要我们找到 ω n n − x \omega_n^{n-x} ωnn−x 的点值,也就是 rev ( n − x ) \text{rev}(n{\rm-}x) rev(n−x),然后放到 rev ( x ) \text{rev}(x) rev(x) 去。怎么办?
别忘了 n = 2 k n=2^k n=2k,所以 n − x n{\rm-}x n−x 其实就是 x x x 保留 lowbit \text{lowbit} lowbit,然后高位按位取反(假设是 k k k 位二进制数)。于是 rev ( n − x ) \text{rev}(n{\rm-}x) rev(n−x) 其实就是 rev ( x ) \text{rev}(x) rev(x) 保留 highbit \text{highbit} highbit,然后低位按位取反。预处理 highbit \text{highbit} highbit 就可以做到了。
再探 FFT \textit{FFT} FFT
可参考论文《再探快速傅立叶变换》和 T L Y \sf TLY TLY 太阳神 的 《课余时间的思考》。
重新理解
循环卷积的本质是对 ( x n − 1 ) (x^n-1) (xn−1) 取模。而
x n − 1 = ∏ j = 0 n − 1 ( x − ω n j ) x^n-1=\prod_{j=0}^{n-1}(x-\omega_n^{\thinspace j}) xn−1=j=0∏n−1(x−ωnj)
是其唯一分解。因此对 ( x − ω n j ) (x-\omega_n^{\thinspace j}) (x−ωnj) 分别取模,再做 CRT \textit{CRT} CRT 即可。也许你可以参考兔兔的博客。
Chirp-Z Transform \text{Chirp-Z Transform} Chirp-Z Transform
也被称为 Bluestain \text{Bluestain} Bluestain 算法。目标是计算 f ( x m ) ( m ∈ [ 0 , n ] ) f(x^m)\;(m\in[0,n]) f(xm)(m∈[0,n]) 。通过 m i = ( m + i 2 ) − ( m 2 ) − ( i 2 ) mi={m+i\choose 2}-{m\choose 2}-{i\choose 2} mi=(2m+i)−(2m)−(2i),可以构造成卷积形式:
f ( x m ) = ∑ i = 0 n − 1 x m i a i = ∑ i = 0 n − 1 x ( m + i 2 ) − ( m 2 ) − ( i 2 ) × a i = x − ( m 2 ) ∑ i = 0 n − 1 x − ( i 2 ) a i × x ( m + i 2 ) \begin{align*} f(x^m) &=\sum_{i=0}^{n-1}x^{mi}a_i\\ &=\sum_{i=0}^{n-1}x^{{m+i\choose 2}-{m\choose 2}-{i\choose 2}}\times a_i\\ &=x^{-{m\choose 2}}\sum_{i=0}^{n-1}x^{-{i\choose 2}}a_i\times x^{{m+i\choose 2}} \end{align*} f(xm)=i=0∑n−1xmiai=i=0∑n−1x(2m+i)−(2m)−(2i)×ai=x−(2m)i=0∑n−1x−(2i)ai×x(2m+i)
那么,想要实现任意长度的循环卷积,只需要将 x = e 2 π n x=e^{2\pi\over n} x=en2π 代入。时间复杂度 O ( n log n ) \mathcal O(n\log n) O(nlogn) 。
NTT \textit{NTT} NTT
全称是 Number Theoretic Transform \text{Number Theoretic Transform} Number Theoretic Transform,数论变换。
它的快速变换和离散变换拥有相同的名字,好奇怪哦。
引子:原根
如果 g φ ( p ) ≡ 1 ( m o d p ) g^{\varphi(p)}\equiv 1\pmod{p} gφ(p)≡1(modp) 且最小的使得 g k ≡ 1 ( m o d p ) g^k\equiv 1\pmod{p} gk≡1(modp) 的正整数 k = φ ( p ) k=\varphi(p) k=φ(p),那么称 g g g 是 p p p 的原根。
特别地,上文中的 k k k 被称为 阶( ord \text{ord} ord),但这不是我们要讨论的内容。
欧拉证明了 2 , 4 , p k , 2 p k ( k ⩾ 1 ) 2,4,p^k,2p^k\;(k\geqslant 1) 2,4,pk,2pk(k⩾1) 有原根,其中 p p p 是奇素数。
模仿 FFT \textit{FFT} FFT
只要满足单位根的若干形式,一切结论都仍然成立。而单位根的本质是 ω n n = 1 \omega_n^{\thinspace n}=1 ωnn=1,所以等比数列求和会得到 0 0 0 。
那么,有了原根 g g g,发现 g φ ( p ) ≡ 1 g^{\varphi(p)}\equiv 1 gφ(p)≡1,所以定义 ω n = g φ ( p ) n ( n ∣ φ ( p ) ) \omega_n=g^{\varphi(p)\over n}\;(n\mid\varphi(p)) ωn=gnφ(p)(n∣φ(p)),就可以完全像 FFT \textit{FFT} FFT 那样进行卷积了。而且所有运算都在整数域(取模的剩余系)下运算,无任何精度误差!
同时我们也发现一个 局限性: φ ( p ) \varphi(p) φ(p) 需要含有足够多的 2 2 2 的次幂,因为 n ∣ φ ( p ) n\mid\varphi(p) n∣φ(p) 时才有定义。最常见的是 p = 998244353 p=998244353 p=998244353,此时 2 23 ∣ φ ( p ) 2^{23}\mid\varphi(p) 223∣φ(p),基本上不会超!
代码实现被放在了多项式运算里,这里就不再贴出。
改进:任意模数
要是模数没有原根怎么办?注意到求出的系数不应超过 a 2 n a^2n a2n,因此另找 3 3 3 个较大模数,用中国剩余定理合并,模意义下也能得到正确结果。
合并的时候好像还会爆 l o n g l o n g \tt{long\;long} longlong,怎么办?
假设已知
{ x ≡ c 1 ( m o d m 1 ) x ≡ c 2 ( m o d m 2 ) x ≡ c 3 ( m o d m 3 ) \begin{cases} x\equiv c_1\pmod {m_1}\\ x\equiv c_2\pmod {m_2}\\ x\equiv c_3\pmod {m_3} \end{cases} ⎩ ⎨ ⎧x≡c1(modm1)x≡c2(modm2)x≡c3(modm3)
我们首先将前两者合并得到
x ≡ k ( m o d m 1 m 2 ) x\equiv k\pmod {m_1 m_2} x≡k(modm1m2)
于是我们假设 x = t m 1 m 2 + k x=t m_1 m_2 + k x=tm1m2+k,由第三个同余方程得 t m 1 m 2 + k ≡ c 3 ( m o d m 3 ) t m_1 m_2 + k \equiv c_3 \pmod {m_3} tm1m2+k≡c3(modm3) 。移项则有
t ≡ ( c 3 − k ) ⋅ m 1 − 1 ⋅ m 2 − 1 ( m o d m 3 ) t\equiv (c_3-k)\cdot m_1^{-1}\cdot m_2^{-1}\pmod {m_3} t≡(c3−k)⋅m1−1⋅m2−1(modm3)
这些运算都是模 m 3 m_3 m3 的,可以轻松进行。求出 ( t m o d m 3 ) (t\bmod m_3) (tmodm3) 也就得到了 x ≡ t m 1 m 2 + k ( m o d m 1 m 2 m 3 ) x\equiv tm_1m_2+k\pmod{m_1m_2m_3} x≡tm1m2+k(modm1m2m3),此时对原模数取模即可。
CNTT \textit{CNTT} CNTT
全称是 Complex NTT \text{Complex NTT} Complex NTT,复数论变换。他可以部分弥补取模 ( 4 k − 1 ) (4k{-}1) (4k−1) 型的质数时 NTT \textit{NTT} NTT 的无力(虽然 任意模数
干翻一切,但是它难打而且慢)。
考虑在某个 G F ( p 2 ) \mathbb{GF}(p^2) GF(p2) 中进行运算,因为此时原根的阶是 ( p 2 − 1 ) = ( p + 1 ) ( p − 1 ) (p^2{-}1)=(p{+}1)(p{-}1) (p2−1)=(p+1)(p−1) 。如果 ( p + 1 ) (p{+}1) (p+1) 里含有足够多的 2 2 2 的幂次,就可以了。
注意到 p ≡ 3 ( m o d 4 ) p\equiv 3\pmod{4} p≡3(mod4) 在高斯整数环 G = Z ( i ) \Bbb G=\Z(i) G=Z(i) 中是素元,因此 p G p\Bbb G pG 是素理想,即得 Z p ( i ) = G / p G \Z_p(i)=\Bbb G/p\Bbb G Zp(i)=G/pG 是整环,结合有限性知其是域。所以在 Z p ( i ) \Z_p(i) Zp(i) 上进行运算即可。但找原根就只能查表了吧。
其实也可以理解为,在多项式环 Z p [ x ] \Z_p[x] Zp[x] 上,对 ( x 2 + 1 ) (x^2{+}1) (x2+1) 取模。核心也在于 ( − 1 ) (-1) (−1) 不是模 p p p 的二次剩余,因此 ( x 2 + 1 ) (x^2{+}1) (x2+1) 是欧几里得环 Z p [ x ] \Z_p[x] Zp[x] 上不可约元素,整环性即证。
定理:交换幺环 R R R 上 DFT \textit{DFT} DFT 可逆的充要条件是 ( ω k − 1 ) ( 1 ⩽ k < φ ) (\omega^k-1)\;(1\leqslant k<\varphi) (ωk−1)(1⩽k<φ) 可逆,其中 φ \varphi φ 是原根 ω \omega ω 的阶。
解释:其充分性是显然的,必要性暂时不清楚,故留作习题。
64-bit \text{64-bit} 64-bit 整数的卷积
模 2 64 2^{64} 264 意义下做卷积。
定义环 R : = Z m o d 2 64 R:=\Z\bmod 2^{64} R:=Zmod264 即 R : = Z / 2 64 Z R:=\Z/2^{64}\Z R:=Z/264Z 。
将环 R R R 拓展到 T : = R [ ω ] m o d ( ω 2 + ω + 1 ) T:=R[\omega]\bmod (\omega^2{+}\omega{+}1) T:=R[ω]mod(ω2+ω+1) 。立方差公式不难验证 ω 3 = 1 \omega^3=1 ω3=1 。
现在考虑在 T [ x ] m o d ( x n − ω ) T[x]\bmod(x^n{-}\omega) T[x]mod(xn−ω) 中做卷积,其中 n n n 是 3 3 3 的幂。
令 y : = x m y:=x^m y:=xm 满足 m r = n mr=n mr=n 且 m ⩾ r m\geqslant r m⩾r 。则原多项式 ∑ a i x i \sum a_ix^i ∑aixi 可以写为
∑ j = 0 r − 1 y j ∑ i = 0 m − 1 a j m + i x i \sum_{j=0}^{r-1}y^j\sum_{i=0}^{m-1}a_{jm+i}x^i j=0∑r−1yji=0∑m−1ajm+ixi
将其视为关于 y y y 的多项式,认为其系数是 T [ x ] m o d ( x 2 m + x m + 1 ) T[x]\bmod(x^{2m}{+}x^m{+}1) T[x]mod(x2m+xm+1),这样显然不会丢失信息。于是我们现在在做 T [ x ] [ y ] m o d ( x 2 m + x m + 1 ) m o d ( y r − ω ) T[x][y]\bmod(x^{2m}{+}x^m{+}1)\bmod(y^r{-}\omega) T[x][y]mod(x2m+xm+1)mod(yr−ω) 的卷积,因为 y r = x n y^r=x^n yr=xn 。
为什么要对 x x x 的多项式取模呢?因为
x 2 m + x m + 1 = ( x m − ω ) ( x m − ω 2 ) x^{2m}+x^m+1=(x^m-\omega)(x^m-\omega^2) x2m+xm+1=(xm−ω)(xm−ω2)
我们只需分别对二者取模,然后用 CRT \textit{CRT} CRT 合并即可。
该 CRT \textit{CRT} CRT 是简单的:设原本为 ( a + b x m ) (a{+}bx^m) (a+bxm),对前者、后者取模分别得到 f , g f,g f,g,有关系式
{ f = a − b ω g = a − b ω 2 ⟹ { a = ω f − g ω − 1 b = f − g ω 2 − ω \begin{cases} f=a-b\omega\\ g=a-b\omega^2 \end{cases} \implies \begin{cases} a={\omega f-g\over\omega-1}\\ b=\frac{f-g}{\omega^2-\omega} \end{cases} {f=a−bωg=a−bω2⟹{a=ω−1ωf−gb=ω2−ωf−g
Comment. C F \rm CF CF 帖子中似乎给出了奇怪的计算式,我不太理解。
最重要的是:此时都有 x 3 m = ω 3 = 1 x^{3m}=\omega^3=1 x3m=ω3=1,即 x x x 是 3 m 3m 3m 次单位根!
所以对 y y y 的循环卷积可行。可惜我们在对 ( y r − ω ) (y^r{-}\omega) (yr−ω) 取模,因此要换元。
若现在对 ( x m − ω ) (x^m{-}\omega) (xm−ω) 取模,那么换元 z : = x 2 m r y z:=x^{2m\over r}y z:=xr2my 有 z r = ω 2 y r z^r=\omega^2y^r zr=ω2yr,只需对 ( z r − 1 ) (z^r{-}1) (zr−1) 取模即可。
若现在对 ( x m − ω 2 ) (x^m{-}\omega^2) (xm−ω2) 取模,则换元 z : = x m r y z:=x^{m\over r}y z:=xrmy 同理。这也是要求 m ⩾ r m\geqslant r m⩾r 的原因。
于是我们有能力作 FFT \textit{FFT} FFT 了。但点值相乘是 T [ x ] m o d ( x m − ω ) T[x]\bmod(x^m{-}\omega) T[x]mod(xm−ω) 内的卷积。所以我们需要递归解决。
反正 ω 2 \omega^2 ω2 和 ω \omega ω 没有本质区别,或者你可以认为是共轭的关系。
分析复杂度。 FFT \textit{FFT} FFT 复杂度为 O ( n log n ) \mathcal O(n\log n) O(nlogn),点值相乘复杂度为 2 r T ( m ) 2r\mathcal T(m) 2rT(m),取 m = r = n m=r=\sqrt{n} m=r=n 有 T ( n ) = 2 n T ( n ) + O ( n log n ) = O ( n log n log log n ) \mathcal T(n)=2\sqrt{n}\mathcal T(\sqrt{n})+\mathcal O(n\log n)=\mathcal O(n\log n\log\log n) T(n)=2nT(n)+O(nlogn)=O(nlognloglogn) 。
Comment. 注意 n ⇝ n n\leadsto\sqrt{n} n⇝n 的递归会使得 log n \log n logn 除以 2 2 2,因此刚好和递归系数中的 2 2 2 抵消。由递归层数 log log n \log\log n loglogn 即得。
Remark. 我们其实可以在任意代数结构上做卷积,但我还没看懂,因此只能贴个链接。