Multiplication algorithm
,才知道目前大数乘法算法主要有以下几种思路:
模拟小学乘法
:最简单的乘法竖式手算的累加型;
分治乘法
:最简单的是Karatsuba乘法,一般化以后有Toom-Cook乘法;
快速傅里叶变换FFT
:(为了避免精度问题,可以改用快速数论变换FNTT),时间复杂度O(N lgN lglgN)。具体可参照
Schönhage–Strassen algorithm
;
中国剩余定理
:把每个数分解到一些互素的模上,然后每个同余方程对应乘起来就行;
Furer’s algorithm
:在渐进意义上FNTT还快的算法。不过好像不太实用,本文就不作介绍了。大家可以参考维基百科
Fürer’s algorithm
1 2 3 4 5 6 7 8
7 8 9 6 5 2 × 3 2 1 1 ----------------- 7 8 9 6 5 2 <---- 第1趟 7 8 9 6 5 2 <---- 第2趟 .......... <---- 第n趟 ----------------- ? ? ? ? ? ? ? ? <---- 最后的值用另一个数组表示
Karatsuba Multiplication Algorithm – Python Code
的图,我们来看看:
我们假设要相乘的两个数是x * y。我们可以把x,y写成:
x = a * 10^{n/2} + b
y = c * 10^{n/2} + d
这里的n是数字的位数。如果是偶数,则a和b都是
n/2
位的。如果n是奇数,则你可以让a是
n/2+1
位,b是
n/2
位。(例如a = 12,b = 34;a = 123,b = 45),那么
x*y
就可以换算为:
\begin{eqnarray*}
&&x * y\\
&=&(a * 10^{n/2}+b) * (c * 10^{n/2}+d)\\
&=&ac * 10^n + (ad + bc) * 10^{n/2} + bd\\
&=&ac * 10^n + [(a+b)*(c+d)-ac-bd] * 10^{n/2} + bd
\end{eqnarray*}
注意最后一步,这个式子倒数第二步中的
(ad + bc)
,没必要另外进行两次乘法,可以使用
((a+b)*(c+d) - ac - bd)
来重复利用前面的两次乘积结果
ac
和
bd
。
也就是说,我们发现最终只需要计算三次乘法
a*c
,
b*d
,
(a+b)*(c+d)
以及六次加法。因此这样复杂度就变为
$$ T(n) = 3T(\frac{n}{2}) + 6n = O(n^{\log_23}) $$
对比之前的计算过程,结果已经呼之欲出了。这里唯一需要注意的两点就是:
(a*d + b*c)
的计算为了防止两次乘法,应该使用之前的计算也就是第一幅图第四步的
③-②-①
这些乘法在算法里应该是递归实现的,数字很大时,先拆分,然后拆分出来的数字还是很大的话,就继续拆分,直到a * b已经是一个非常简单的小问题为之。这也是分治的思想。
我们举例来尝试一下这种算法,比如计算
12345 * 6789
,我们让
a = 12
,
b = 345
。同时
c = 6
,
d = 789
。也就是:
12345 = 12 · 1000 + 345\\
6789 = 6 · 1000 + 789
那么
a*c
,
b*d
的结果如下:
\begin{align*}
z_2 &= a*c = 12 × 6 = 72\\
z_0 &= b*d = 345 × 789 = 272205 \\
z_1 &= ((a+b)*(c+d) - a*c - b*d)\\
&= (12 + 345) × (6 + 789) − z_2 − z_0 = 283815 − 72 − 272205 = 11538
\end{align*}
最终结果就是:
\begin{align*}
& result = z_2 · 10^{2*3} + z_1 · 10^3 + z_0 \\
& result = 72 · 10^6 + 11538 · 10^3 + 272205 = 83810205. \\
\end{align*}
以上就是使用分治的方式计算乘法的原理。上面这个算法,由 Anatolii Alexeevitch Karatsuba 于1960年提出并于1962年发表,所以也被称为 Karatsuba 乘法。
根据上面的思路,实现的Karatsuba乘法代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
public static long karatsuba (long num1, long num2) { if (num1 < 10 || num2 < 10 ) return num1 * num2; int size1 = String.valueOf(num1).length(); int size2 = String.valueOf(num2).length(); int halfN = Math.max(size1, size2) / 2 ; long a = Long.valueOf(String.valueOf(num1).substring(0 , size1 - halfN)); long b = Long.valueOf(String.valueOf(num1).substring(size1 - halfN)); long c = Long.valueOf(String.valueOf(num2).substring(0 , size2 - halfN)); long d = Long.valueOf(String.valueOf(num2).substring(size2 - halfN)); long z2 = karatsuba(a, c); long z0 = karatsuba(b, d); long z1 = karatsuba((a + b), (c + d)) - z0 - z2; return (long )(z2 * Math.pow(10 , (2 *halfN)) + z1 * Math.pow(10 , halfN) + z0); }
Karatsuba 算法是比较简单的递归乘法,把输入拆分成 2 部分,不过对于更大的数,可以把输入拆分成 3 部分甚至 4 部分。拆分为 3 部分时,可以使用下面的
Toom-Cook 3-way
乘法,复杂度降低到 O(n^1.465)。拆分为 4 部分时,使用
Toom-Cook 4-way
乘法,复杂度进一步下降到 O(n^1.404)。对于更大的数字,可以拆成 100 段,使用
快速傅里叶变换FFT
,复杂度接近线性,大约是 O(n^1.149)。可以看出,分割越大,时间复杂度就越低,但是所要计算的中间项以及合并最终结果的过程就会越复杂,开销会增加,因此分割点上升,对于公钥加密,暂时用不到太大的整数,所以使用 Karatsuba 就合适了,不用再去弄更复杂的递归乘法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
public class LeetcodeTest { public static void main (String[] args) { String a = "98" ; String b = "21" ; char [] charArr1 = a.trim().toCharArray(); char [] charArr2 = b.trim().toCharArray(); int [] arr1 = new int [charArr1.length]; int [] arr2 = new int [charArr2.length]; for (int i = 0 ; i < charArr1.length; i++){ arr1[i] = charArr1[i] - '0' ; } for (int i = 0 ; i < charArr2.length; i++){ arr2[i] = charArr2[i] - '0' ; } int [] result = LeetcodeTest.bigNumberMultiply2(arr1, arr2); System.out.println(a + " * " + b + " = " + Arrays.toString(result).replace(", " , "" )); } }
GNU的GMP
,GMP是The GNU MP Bignum Library,是一个开源的数学运算库,它可以用于任意精度的数学运算,包括有符号整数、有理数和浮点数。它本身并没有精度限制,只取决于机器的硬件情况。许多著名的计算机代数系统如Axiom, Maple, Mathematica, Maxima等的底层高精度整数运算都是基于GMP实现的。
设n 为乘数的位数, 就目前已知的情况而言, 不同乘法算法的时间复杂度可以从平凡的$O(n^2)$(普通乘法), $O(n^{log_2 3})$(Karatsuba 乘法), $O(n^{log_3 5})$ (Toom-3 乘法),$O(n {\log^* n})$ (复数域上的FFT),其中
$$ log^* n = log n(log log n)(log log log n) ··· , $$
和$O(n(log n)(log log n))$(有限域上的FFT), 其中
“有限域上的FFT”
的时间复杂度已经相当接近线性了。
但是这些乘法算法中复杂度较低的算法往往有较大的常数因子, 因此如果乘数的位数较少,普通乘法反而是最快的, 所以实用中常常将这些不同的乘法算法结合起来使用, 每次做乘法时都根据相乘两数的大小动态地选择具体采用哪一种算法, 而每种算法的最佳适用范围往往依赖于具体实现和硬件环境, 因此一般直接通过实验来确定。
[BigInteger - Java7]
(见1165行)
在 Java 8 里面,根据两个因数的大小,有三种乘法。源代码:
[BigInteger - Java8]
(见1464行):
当两个因数均小于 $2^{32\times 80}$ 时,用二重循环直接相乘,复杂度为$O(n^2) $,n为因数位数(下同);
否则,当两个因数均小于$2^{32\times 240} $时,采用 Karatsuba algorithm,其复杂度为$O(n^{\log_2 3}) \approx O(n^{1.585})$;
否则,采用 Toom-Cook multiplication,其复杂度为$O(n^{\log_3 5}) \approx O(n^{1.465})$。
其中,Java8中的源代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
private static final int MULTIPLY_SQUARE_THRESHOLD = 20 ;private static final int KARATSUBA_THRESHOLD = 80 ;private static final int TOOM_COOK_THRESHOLD = 240 ;public BigInteger multiply (BigInteger val) { if (val.signum == 0 || signum == 0 ) return ZERO; int xlen = mag.length; if (val == this && xlen > MULTIPLY_SQUARE_THRESHOLD) { return square(); } int ylen = val.mag.length; if ((xlen < KARATSUBA_THRESHOLD) || (ylen < KARATSUBA_THRESHOLD)) { int resultSign = signum == val.signum ? 1 : -1 ; if (val.mag.length == 1 ) { return multiplyByInt(mag,val.mag[0 ], resultSign); } if (mag.length == 1 ) { return multiplyByInt(val.mag,mag[0 ], resultSign); } int [] result = multiplyToLen(mag, xlen, val.mag, ylen, null ); result = trustedStripLeadingZeroInts(result); return new BigInteger(result, resultSign); } else { if ((xlen < TOOM_COOK_THRESHOLD) && (ylen < TOOM_COOK_THRESHOLD)) { return multiplyKaratsuba(this , val); } else { return multiplyToomCook3(this , val); } } }
我们可以看到,Java8依据两个因数的量级分别使用Karatsuba algorithm 和 Toom-Cook multiplication 算法计算大数乘积。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
private static BigInteger multiplyKaratsuba (BigInteger x, BigInteger y) { int xlen = x.mag.length; int ylen = y.mag.length; int half = (Math.max(xlen, ylen)+1 ) / 2 ; BigInteger xl = x.getLower(half); BigInteger xh = x.getUpper(half); BigInteger yl = y.getLower(half); BigInteger yh = y.getUpper(half); BigInteger p1 = xh.multiply(yh); BigInteger p2 = xl.multiply(yl); BigInteger p3 = xh.add(xl).multiply(yh.add(yl)); BigInteger result = p1.shiftLeft(32 *half).add(p3.subtract(p1).subtract(p2)).shiftLeft(32 *half).add(p2); if (x.signum != y.signum) { return result.negate(); } else { return result; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
private static BigInteger multiplyToomCook3 (BigInteger a, BigInteger b) { int alen = a.mag.length; int blen = b.mag.length; int largest = Math.max(alen, blen); int k = (largest+2 )/3 ; int r = largest - 2 *k; BigInteger a0, a1, a2, b0, b1, b2; a2 = a.getToomSlice(k, r, 0 , largest); a1 = a.getToomSlice(k, r, 1 , largest); a0 = a.getToomSlice(k, r, 2 , largest); b2 = b.getToomSlice(k, r, 0 , largest); b1 = b.getToomSlice(k, r, 1 , largest); b0 = b.getToomSlice(k, r, 2 , largest); BigInteger v0, v1, v2, vm1, vinf, t1, t2, tm1, da1, db1; v0 = a0.multiply(b0); da1 = a2.add(a0); db1 = b2.add(b0); vm1 = da1.subtract(a1).multiply(db1.subtract(b1)); da1 = da1.add(a1); db1 = db1.add(b1); v1 = da1.multiply(db1); v2 = da1.add(a2).shiftLeft(1 ).subtract(a0).multiply( db1.add(b2).shiftLeft(1 ).subtract(b0)); vinf = a2.multiply(b2); t2 = v2.subtract(vm1).exactDivideBy3(); tm1 = v1.subtract(vm1).shiftRight(1 ); t1 = v1.subtract(v0); t2 = t2.subtract(t1).shiftRight(1 ); t1 = t1.subtract(tm1).subtract(vinf); t2 = t2.subtract(vinf.shiftLeft(1 )); tm1 = tm1.subtract(t2); int ss = k*32 ; BigInteger result = vinf.shiftLeft(ss).add(t2).shiftLeft(ss).add(t1).shiftLeft(ss).add(tm1).shiftLeft(ss).add(v0); if (a.signum != b.signum) { return result.negate(); } else { return result; } }
华为OJ机试题目:两个大整数相乘(纯C语言实现两个大整数相乘,两种方法实现大数相乘)
java的BigInteger的乘法运算是用什么算法实现的?
大数乘法,在算法上,主要有几种思路?
算法理解 — 大数相乘问题
KaraTsuba乘法 — 高效的大数乘法
大数相乘(from滴答滴答百度空间)
大整数运算
维基百科:Multiplication algorithm
Karatsuba Multiplication Algorithm – Python Code
Toom-Cook 3-Way Multiplication - GMP Documentations
扩展欧几里得算法与中国剩余定理