问题引入
求多项式乘法
关于多项式
多项式除了用系数表示法外,还可以使用点值表示法。显然用 n+1 个点就能表示一个 n 项多项式。
表示为:
$ f(x)={(x0,y0),(x1,y1),⋯,(xn,yn)} $
DFT / IDFT
对系数表示的 n 项多项式乘法,朴素算法需要 O(n2) 的复杂度,然而可以发现,对于同一组 x 所表示的两个点值表示多项式,可以轻易地再在 O(n) 时间内完成乘法计算。也就是:
f(x)={(x0,f(x0)),(x1,f(x1)),⋯,(xn,f(xn))}
g(x)={(x0,g(x0)),(x1,g(x1)),⋯,(xn,g(xn))}
f(x)g(x)={(x0,f(x0)g(x0)),(x1,f(x1)g(x1)),⋯,(xn,f(xn)g(xn))}
而把系数表示转化为点值表示的操作叫做DFT,反过来叫IDFT。那么只需要找到高效的DFT和IDFT方法,就能得到高效的多项式乘法。
我们可以指定DFT过程中的取的不同点值 Xi 为一些具有特殊性质的数,从而利用这些性质加速运算。
单位复根
复数基础
首先需要对复数有一个简单的了解。
几何意义: z=a+bi 对应复平面内的点 (a,b)
模长: ∣z∣=a2+b2
幅角: θ=arctanxy
欧拉公式: eiθ=cosθ+isinθ ,这对应着复平面上的一个单位向量。
那么 z=∣z∣eiθ=r(cosθ+isinθ) ,其中 θ 为 z 的幅角。
进而得到,复数相乘就是模长相乘,幅角相加。
单位复根
现在考虑 xn=1 的所有复根。首先显然有 ∣x∣=1 ,那么设 x=eiθ ,得到
$ einθ=cosnθ+isinnθ=1 $
也就是
nθθ=2kπ=n2kπ(k=0,1,⋯,n−1)
规定其中的 en2π 为“单位根” ωn ,那么原方程的所有复根就可以表示成
$ x=ωnk(k=0,1,⋯,n−1) $
现在考察其几何意义,不难发现这些根所表示的点就是绕单位圆一周 n 等分的点。
比如三次的情形:
一些重要的性质:
ωnnωnkω2nk+n=1=ω2n2k=−ω2nk
结合一下几何意义,这些结论是比较显然的。
而在DFT的过程中,带入的 {xi} 正是 {ωn0,ωn1,⋯,ωnn−1}
分治
知道了带代入哪些值,求解的过程就需要合理地利用这些值的性质。
考虑分治,将多项式分为奇次项和偶次项处理。
举个例子,对于一共 8 项的多项式
f(x)=a0+a1x+a2x2+a3x3+a4x4+a5x5+a6x6+a7x7
按照次数的奇偶来分成两组,然后右边提出来一个 x
f(x)=(a0+a2x2+a4x4+a6x6)+(a1x+a3x3+a5x5+a7x7)=(a0+a2x2+a4x4+a6x6)+x(a1+a3x2+a5x4+a7x6)
分别用奇偶次次项数建立新的函数
G(x)H(x)=a0+a2x+a4x2+a6x3=a1+a3x+a5x2+a7x3
那么原来的 f(x) 用新函数表示为
f(x)=G(x2)+x×H(x2)
利用单位复根的性质得到
f(ωnk)=G((ωnk)2)+ωnk×H((ωnk)2)=G(ωn2k)+ωnk×H(ωn2k)=G(ωn/2k)+ωnk×H(ωn/2k)
同理可得
f(ωnk+n/2)=G(ωn2k+n)+ωnk+n/2×H(ωn2k+n)=G(ωn2k)−ωnk×H(ωn2k)=G(ωn/2k)−ωnk×H(ωn/2k)
由此我们发现
$ {H(ωn/20),⋯,H(ωn/2n/2−1)}{G(ωn/20),⋯,G(ωn/2n/2−1)} $
这两个子问题的形式与原问题
$ {H(ωn0),⋯,H(ωnn−1)} $
是完全相同的,而且两个子问题的答案能在 O(n) 时间内合并得到原问题的答案。 于是总时间复杂度为 O(nlogn) (参考归并排序)这就是DFT操作。
另外需要注意将 f 的高次补 0 以满足长度 n 是 2 的整数次幂。
参考代码
IDFT
现在我们已经用 O(nlogn) 的时间得到了两个多项式的点值形式,从而通过点值相乘得到了答案多项式的点值形式。还剩下通过IDFT将答案多项式的点值形式转化为系数形式。要如何操作?
其实只需要简单地把单位复根 ωn=en2π 变为其倒数 ωnk1=e−n2π ,再进行一遍DFT就可以了(即:将有第一遍DFT得到的点值看作“系数”,在此基础上带入 ωnk1(k=0,1,⋯,n−1) 求值)。
下面证明:
原多项式:
$ f(x)=∑i=0n−1aixi $
得到的点值:
$ yi=f(ωni)=∑j=0n−1ajωnij $
将点值 yi 作为系数得到的新多项式:
$ g(x)=∑i=0n−1yixi $
在新多项式里代入:
g(ωn−k)=i=0∑n−1yiωn−ik=i=0∑n−1j=0∑n−1ajωnijωn−ik=j=0∑n−1aji=0∑n−1(ωnj−k)i
记 S(ωna)=∑i=0n−1(ωna)i 。
当 a=0(modn) 时, S(ωna)=n 。
当 a=0(modn) 时,我们错位相减
S(ωna)ωnaS(ωna)S(ωna)=i=0∑n−1(ωna)i=i=1∑n(ωna)i=ωna−1(ωna)n−(ωna)0=0
也就是说
S(ωna)={n,a=00,a=0
那么代回原式
g(ωn−k)=j=0∑n−1ajS(ωnj−k)=ak⋅n
所以只需要写一个函数,传入参数 op , op == 1 时为DFT, op == -1 为IDFT,进行IDFT时再将各个结果 /n 即可。
另有方法二,原理基本相同
非递归
为了提高运行效率,可以将FFT改成非递归的写法。
每次递归实际上就是将奇数位置和偶数位置的系数分别放在头尾。
{x0,x1,x2,x3,x4,x5,x6,x7}{x0,x2,x4,x6},{x1,x3,x5,x7}{x0,x4}{x2,x6},{x1,x5},{x3,x7}{x0}{x4}{x2}{x6}{x1}{x5}{x3}{x7}
通过手动模拟可以发现,这样的操作实际上就是对所有数以最后一个二进制位为第一关键字、倒数第二个二进制位为第二关键字……进行的排序,即按照二进制对称翻转后的数进行排序。我们称这个变换为位逆序置换(蝴蝶变换)。
记 R(x) 为 x 位逆序置换后的结果。
显然朴素算法能再 O(nlogn) 的时间内求解 R(x) ,现在考虑递推优化。
可以发现 R(x) 与 R(x/2) 只有一个二进制位的差别,于是简单地模拟一下就可以写出 k 个二进制位时的递推式:
R(x)=⌊2R(⌊2x⌋)⌋+(xmod2)×2len
其中 len=2k 。
进行为逆序置换的代码:
(其中的 if (i < rev[i]) )是为了防止重复交换)
示例代码
模板:高精度乘法
参考
https://oi-wiki.org/math/poly/fft/