多精度里FNT和SSA的点滴.
Karatsuba,TOOM3,4.5...Toom-Cook
可以看成是插值算法的逐步扩展.
比如TOOM3,
F(x) = ax^2 + bx + c
G(x) = dx^2 + ex + f
Q(x) = F(x) * G(x)
Q(x) = Ax^4 + Bx^3 + Cx^2 + Dx + e
对Q(x),x=取5个不同值ri,i = 0,1,2,3,4,即可一矩阵(行列式)
|r0^4 r0^3 r0^2 r0 1| |A| |F(r0) * G(r0)|
|r1^4 r1^3 r1^2 r1 1| |B| |F(r1) * G(r1)|
|r2^4 r2^3 r2^2 r2 1| * |C| = |F(r2) * G(r2)|
|r3^4 r3^3 r3^2 r3 1| |D| |F(r3) * G(r3)|
|r4^4 r4^3 r4^2 r4 1| |E| |F(r4) * G(r4)|
通过解上面矩阵既可以得到A,B,C,D,E,即Q(x)多项式系数.
这个推广后即是Toom-Cook算法.
FFT则是Toom-Cook对取值的一个特化.
对于长度为N的FFT,其取值为复数域内N次方根,
|r0^4 r0^3 r0^2 r0 1|
|r1^4 r1^3 r1^2 r1 1|
|r2^4 r2^3 r2^2 r2 1| 就是一个范德蒙德(Vandermonde)矩阵.
|r3^4 r3^3 r3^2 r3 1|
|r4^4 r4^3 r4^2 r4 1|
其求解只需要nlogn就可以了.具体的解法随便一本关于傅里叶变换的书都会有介绍.
题外话:FFT还有一个特性就是变换后,频域和时域相互映射起来,这个在工程某些方面这个特性是很有用的.比如滤波,信号处理,数据压缩等等多个方面.有很大的作用(其他如余弦变换,小波变换也有类似的功能,但在有限域内这个特性一般都没有了),在高精度计算里,这个特性并没有用上.略过不表.
FNT,一般指代的是快速数论变换(也有人指FNT是指快速数论变换里面的费马变换,但是由于费马素数太少,实际的应用只存在特定领域),
这里基于代码简单化起见,特指是基2的快速数论变换,但要说明的是,FNT不仅仅包含基2的快速数论变换.
FNT实际只是FFT在有限域的一种表现形式.类比说就是FFT其插值是取复数域内的N次方根,FNT其插值是取某个剩余系内的N次方根.其余的运算和FFT基本一致.
FNT额外的好处就是由于在有限域下计算,只要数据不产生溢出,里面的精度是不会丢失的.这个要比FFT要好.
FNT里面的相关参数主要有M,N,α(或记作r),M为模,N为变换长度, α(r)为N次方根(MOD M下),只要选取的M存在N次方根,这个选取M就是可用的.就这点来说,这个选取还是比较宽松的,如果还需要考虑计算的效率问题(主要是a * b % M这个过程),选取的M则需要好好考虑.
选择M基于下面一些考虑.
变换长度为N,则必须存在N次方根(这个是FNT的内在要求)
高精度计算里,卷积的结果范围为N * (R - 1)^2,则必须M > N * (R - 1)^2 (R为进制)
计算尽可能简单.(这个是高精度要求,也可以算是卷积要求)
附:计算尽可能简单不等于尽可能小.比如M = 2^32 ±1 这个的计算就都比M = 123456789要简单.(实际运行效率要求)
显然,这里没有要求M必须是素数.这就留出了较大的优化空间.
优化方向,主要是选取M方面着手.
1. M尽可能小
2. CRT(中国剩余定理),
3. MOD M尽可能简单
4. 选取比较特殊的M,
1:这个略过(感觉废话),
2:这个主要是aploat主要干的事,(我的代码主要也是基于这个)
3:这个主要是GMP的优化方向,(不是太旧的版本是这样,4.x之前的据说不是)
4: Fürer's algorithm似乎就是基于这个方向,但是没有看懂原理
比如M = 65537(2^16 + 1),在十进制下其变换长度最大为 65536/81 > 512,即10进制下最大变换长度为512,
比如1649267441665(3*2^39 + 1)/ 81 > 2^34,在10000进制下, 164926744166/ 9999^2 > 2^14
如果M不是素数,M = m1 * m2 * m3 * m4....,就实际应用效率而言,一般取M为3个接近自然位长(32位,64位机类似去接近64位)素数乘积.且每个素数都存在N次方根.即可.
比如 apfloat里面使用的3个素数就是2113929217, 2013265921, 1811939329,(源代码为2.41版本)
这里考虑加减法上的优化,都没有使用超过2^31的数.
自身代码的选择则是2013265921, 1811939329,1711276033,实际选取的原则其实并没有什么不同.
如不考虑乘法复杂度就乘法次数而言,直接MOD M运算的乘法次数要小于分三段,由于M超出了自然位长(80+位,接近3个自然位长,因此分三段最后再用CRT合并,这个中途都是32位长,只有最后CRT时才是3个自然位长长度,这样可能会比直接一次3个自然位长的FNT更好的效率.
较为细致较为精确的复杂度后面(可能)会进行补充说明.
GMP应用的是SSA,我的理解是M为2^x + 1,r = 2,的FNT的一个特化.
Modular arithmetic and the FFT
Input:0<= A,B < 2^n + 1,and interger K = 2^k,such that n = MK
Output C = A*B mod(2^n + 1)
1: decompose A = ∑ai*2^iM (i = 0,1,2,..K-1) with 0 <= ai < 2^M except that
0 <= a(K-1) <= 2^M
2: decompose B similarly
3: choose n' >= 2n / K + k,n'multiple of k: let θ = 2^(n'/K), ω = θ^2
4: forj for 0 to K - 1 do
5: (aj,bj) ← (θ^j * aj,θ^j * bj) mod 2^n' + 1
6: a ← ForwardFFT(a,ω,K),b ← ForwardFFT(b,ω,K)
7: for j from 0 to K - 1 do (call FFTMulMod)
8: cj = ajbj mod (2^n' + 1) (recursively if n' is large)
9: c ← BackwordFFT(a,ω,K)
10: for j from 0 to K - 1 do
11: cj ← cj/(K*θ^j) mod 2^n' + 1
12 if(cj >= (j + 1)2^2M then
13 cj ← cj - (2^n' + 1)
14 C = ∑ ci*2^iM (i = 0,1,2,....K - 1)
上面这个的流程可以算是说得比较清楚的了.
取一个适合大的简单的模M(2次幂加1),根也是2的幂.因此当中的计算应该算是简单的.
上面的流程写成递归的形式比较方便.
另外还有一个优化方法是选取的M是某个小素数的幂.但这个了解不多.
关于FNT,一些比较细致的常熟级复杂度优化会在后面进行补充说明
我的代码已经全部放上了github上了.另外也在51nod上的大数乘法里使用有关于FNT的具体代码AC通过了.
至于GMP和Apfloat库源代码可以在其官方上下载.
看不到公式的请查看附件
后面专门针对FNT一个具体实践例子的复杂度进行分析一下.
默认进制为R = 10^9,变换长度为N,(N <= 2^25),
那么选取的M的范围为M > N*(R �C 1)^2,不妨假设其N次方根其中任意一个为r
就参数选取通用而言,可以直接考虑N = 33554432 (2^25)时的状态即可
且只考虑到计算得到准确的卷积即认为所需计算复杂度结束(实际多精度计算还有一个线性的进位处理,但这个将不算入分析算法复杂度之内),而且内部加法基本上和乘法将保持线性关系,因此计算具体的复杂度的时候,我将只考虑乘法计算.
显然2^64 < M ,且M < 2^96内必行有合适的数可选,那可以认为2^64 < M < 2^96为比较合适的取值范围.
那么计算卷积
F(x) * G(x) = Q(x),
里面有
FNT(F(x))
FNT(G(x))
点乘F(x) = FNT(F(x)) * FNT(G(x))
IFNT(F(x))
里面的2^96模乘法计算量为
NlogN
NlogN
N
NlogN + N (后面的+N是由于逆变换后还需要一次/ N的运算才是准确卷积结果)
大约比较准确的复杂度为
3NlogN + 2N次2^96内的模乘.
不妨假设一次2^96内的模乘所需实际等价于s次32范围内的模乘时间,一般认为这s > 3
即具体的时间复杂度
s * (3NlogN + 2N)
后面的优化基本以s = 3为基准参考点.
9NlogN + 6N
比如Apfloat里面选取M = m1 * m2 * m3,最后再合并一次.
显然是结构都是类似的,只是使用了3个模m1,m2,m3则具体的复杂度为
3 *(3 NlogN + 2N) + kN, k实际为CRT合并的花费,我认为k实际一般不超过2-3的小常数
即为9NlogN + 6N + kN
3 *(3 NlogN + 2N) + kN - s * (3NlogN + 2N) =kN - (s �C 3) * (3NlogN + 2N)
= N(k �C (s �C 3) * (3logN + 2)),
一般来说,N都会相对比较大,因此,实际s并无需比3大很多,这样的优化就会有意义的了.
因此这个优化通常是有意义的.
这个优化就是一般意义上的FNT + CRT的表述.
另一种优化方向就是利用一个比较低级的优化计算,Comba算法.
实际上,这个从一定意义上看,这个只是一种硬乘法,和硬乘法的乘法次数是相同的.
只是这个比一般的硬乘法额外的好处就是可以比较容易处理成并行处理.
比如 F(x) = Ax^2 + Bx + C, G(x) = Dx^2 + Ex + F;
Q(x) = ax^4 + b x^3 + c x^2 + d x + e
A B C
D E F
------------------------------------------------------------------------------
AF BF CF
AE BE CE
AD BD CD
------------------------------------------------------------------------------
a = AD
b = AE + BD
c = AF + BE + CD
d = BF + CE
e = CF
这里两两乘积都是可以并行处理的.
当任意两两相乘都可以使用相同变换长度时,对于A,B,C,D,E,F变换都是可以通用的,只需要1次变换即可,这样前面两次需要平方次数的变换,可以用线性次数的变换取代,这样的优化在F(x)和G(x)长度差异很大时或者只比2次幂大一点点时是很有意义的.
不妨假设AD使用的变换长度为n, ,F(x)分解为p个n长度的变换,G(x)分解为q个长度为n的变换.
那么这里的复杂度为
(p + q) nlogn + p * q nlogn + p * q * 2n
作为基准则为不小于F(x) * G(x)结果长度的N
即
NlogN + 2N
通常来说,p,q相差较大时差距,这样的优化是有意义的.
而且这个是在M意义下进行的,和上面CRT并不冲突.
如果引用
M = m1 * m2 * m3
即具体的复杂度为
6 * (p + q) nlogn + 3 * p * q nlogn + p * q * 2n
由于这里F(x) * G(x)通常都不是刚好2次幂,因此N的选取和p,q,n通常不存在等式关系.
因此我对p,q,n的选取是在一定误差范围内,搜索最佳近似解(有点绕,有误差而且还是最佳近似),而且即使乘积刚好是2次幂,也是可以通过搜索确定是否还存在更好的办法.
附:这样的优化即使是F(x) == G(x)也是有意义的,通常情况这样要比F(x) != G(x)时,效果要差些,因为F(x) == G(x)下,FNT的效率要比F(x) != G(x)要好很多,这样对使用comba算法优化上优势削弱了.但并不是完全没有意义的.
具体的代码实现是先建立一个表,查表得到最佳近似,然后对查表得到的p,q,n再修正一次就
FNT和SSA的比较.
通常情况下,上面两种方法的优化在没有并行时是没什么意义的,有时这样的处理反而变得慢了.
而SSA的结构和FNT是类似的.因此可以认为其复杂度也同样为s * (3NlogN + 2N)
但是由于里面使用的模都是2^k + 1,根都是2次幂,因此实际上s < 3,(事关模乘里面最耗时间的求余,被用移位和加法求余来代替了,事实上估计是相当于2次2^96范围内的乘法和若干次移位和加法修正)
在考虑到充分并行的情况下
s * (3NlogN + 2N)
,由于前面两次变换是可以并行的,
其复杂度实际上相当于s (2NlogN + 2N)
而FNT在考虑到充分并行的情况下
9NlogN + 6N可以压缩到
2NlogN + 6N,
6 * (p + q) nlogn + 3 * p * q nlogn + p * q * 2n
则可以压缩到
nlogn + nlogn + p*q * 2n = 2nlogn + p * q * 2n
这样通常是远小于s (2NlogN + 2N)
当然这个是仅仅考虑完美的并行运行的极限状态.
实际效果由于一般的PC的CPU核数并不多,其速度提醒并没有那么恐怖的提升.
但是通常在并行比较可行时,其速度要快于SSA.