from:2017 CCF计算机课程改革导教班. 陈道蓄
11
大整数相乘 – 比长乘更快
小学里就教过整数乘的算法。要计算两个正整数a,b的乘积,你用b中每一位依次乘a,并将结果逐行排列,按b的相应位对齐。将各行相加便得到a´b。以下即是一个例子:
用b中的一位乘a称为短乘,整个计算则称为长乘。如果a,b都是位数非常多的整数,即使用现代计算机计算也得费点儿时间。可是处理非常大的整数是计算机应用中经常需要解决的问题,例如互联网通信时用的密码。还可以举出其它例子,例如求几何或代数问题的可靠解。
幸运地是我们有更好的方法计算乘积。这对于相关应用当然是好消息,但这方法本身也值得关注。长乘是我们都熟悉并认为很自然的算法,居然还能有实质性的改进真令人吃惊。
接下来我们将考察:
1, 用长乘计算两个整数的乘积需要多少代价?
2, 怎么能做得更快?
计算机科学家并不用分、秒来度量算法执行的时间代价,因为这与硬件、编程语言以及算法在机器中执行的细节有关 (过一年计算机硬件可能速度更快)。度量算法时间代价时我们考虑“基本操作”执行的次数。所谓基本操作是指计算机或者人“一步”可以执行的操作。这里我们将(十进)数字,即0,1,2, …, 9,的算术计算作为基本操作。
1, 两个数字相乘:两个数字x,y的乘积可以用另外两个数字u,v表示:x´y=10´u+v。你肯定还记得乘法表。
例子:假设数字x=3, y=7, 则x´y=3´7=21=10´2+1,所以得到的结果为数字u=2, v=1。假如给的两个数字是x=3, y=2,结果数字则为u=0, v=6。
2, 三个数字相加:假设给三个数字x, y, z,它们的和可以用两个数字u,v表示:x+y+z=10´u+v。很快大家就会知道为什么我们一次加三个数字。
那么在长乘中基本操作执行多少次呢?要回答这个问题,我们得先看看长乘中用到的两个更简单的算法:加法和短乘。
长整数的加法
两个整数a,b相加需要多少代价?当然这与两个数有多少位相关。假设a,b均为n位。如果两个数不一样长,可以在短的数前面补零,使其与另一个数位数相等。我们将一个数写在另一个数上面。从右向左,逐位相加。其结果10´u+v告诉我们将v作为本列的结果数字,而u则进位至下一列。例如,给两个四位数:a=6917, b=4269,其算式如下:
最左列的进位1不参加如何计算了,直接写在结果的第一位。每列执行一次基本运算,即加三个数字的加运算,总的基本运算次数为n。
短乘: 与一位数相乘
做长乘时,我们用被乘数a去乘乘数中的每一位y。现在我们通过中间结果来仔细看看这个短乘:从右向左,用a的每一项x去乘y,结果10´u+v写为单独一行,并使v与x列对齐。当a中所有位计算完毕,则将全部两位的中间结果加起来,写为一行,这就是短乘的结果。
我们在回顾一下前面那个长乘的例子:5678´4321。要做的第一个短乘如下:
我们执行了多少次基本操作呢?a中的每一位均进行一次个位乘法,所以共执行n次。在上面的例子中,对于5678中的四位数必须执行四次基本乘操作。然后必须将所有中间结果加起来。这里共有n+1列。最右边一列只有一个数,不用加,直接抄下来。其它各列有两个数以及一个进位值,执行一次基本加操作即可。因此基本加法操作执行n次。这样,将一个n位整数与一个一位数相乘,基本操作执行次数为2n。
长乘的分析
现在我们来分析两个n位整数a,b相乘需要执行多少次基本操作。如果两个数位数不同,可以通过在短的一个前面补零的方法使它们位数相等。
对b中每一位y做一次短乘a´y。如前所述,这需要2´n次基本操作。而b有n位,所以一次长乘要进行n次短乘,总共要执行n´(2´n)次基本操作,即2n2次。
短乘的结果必须与b中的相应为对齐。为了分析简单,我们在空位中均补0。
短乘的结果用前面说的方法相加,先将第一和第二行加起来,在与第三行相加,余此类推,直到全部n行加起来。这需要n-1次长加。如上例,n=4,则需要3次长加:22712000 + 1703400 = 24415400, 24415400 + 113560 = 24528960 以及 24528960 + 5678 = 24534638。
这n-1 次长加需要多少次基本操作呢?要回答这个问题必须知道这一串加法的中间结果又多少位。稍加思考就可以看出: a´b最多2n位,而每次做加法,位数只可能增加,而不可能减少。所以,所有的中间结果位数最多不会超过最终结果的位数,即2n位。因此,我们需要执行最大2n位的长加共n-1次。根据对长加的分析可知,其基本操作执行次数最多(n-1)´(2´n)=2´n2-2´n。加上短乘的2´n2次,两个n位整数相乘需要的基本操作总共为4´n2-2´n次。
让我们通过一个例子看看这意味着什么。假如要相乘的两个数真是很大,例如各有100000位。这两个数一次长乘几乎需要400亿次基本操作,其中包括100亿次个位数乘。结果中每一位数平均需要20万次基本操作,这显然比例太高了。随着位数增加,比例甚至还会更高。100万位的整数相乘几乎需要执行4万亿次基本操作,乘积中每一位数需要的基本操作平均达到200万次。
Karatsuba方法
我们可以采用更聪明的方法。下面我们要介绍的算法可以用少得多的基本操作完成两个n位整数的乘法。这个方法是俄罗斯数学家Anatolii Alexeevitch Karatsuba提出来的,算法也由此得名。首先我们用这个方法计算一位,两位或四位数的乘法,然后再扩展到任意多位。
最简单的情况当然是计算两个一位数的乘积,即n=1,只需要执行一次基本乘法,即个位数乘,立即可以得到结果。
接下来看n=2的情况,即计算两个两位整数a和b的乘积。我们将a,b按照其十进制表示的两个数字拆分为两半:
a=p´10+q, b=r´10+s
例如,对于数a=78和b=21,可以得到:
P=7, q=8, 而 r=2, s=1
这样乘积a´b就可以用以上数字表示为:
a´b=(p´10+q)´(r´10+s)
=(p´r)´100+(p´s+q´r)´10+q´s:
考虑上面的例子: a=78, b=21,可以得到:
78×21=(7×2)×100+(7×1+8×2)×10+8×1=1638
将乘积改写为上面的形式显示了如何用4次个位数乘加上加运算计算两位数a,b的乘积。基本操作的次数和我们前面分析的是一样的。
Karatsuba提出的思想使得我们计算两个两位整数a,b的乘积之需要三次个位数乘。这三次乘用于计算以下三个辅助乘积:
u=p´r,
v=(q-p)´(s-r),
w=q´s
请注意v的计算,这里必须引入新的基本操作,即个位数减法。在计算v时用两次减法,其结果(q-p)和(s-r)一定还是个位数,但可能是负数。求它们的乘积只要一次个位数乘,但要应用乘法的符号规则(如“负负得正”等)。
为什么这样做有助于计算a,b的乘积呢?请看如下的式子:
u+w-v=p´r+q´s-(q-p)´(s-r)=p´s+q´r
Karatzuba的诀窍就是利用这三个辅助乘积u,v,和w来表示乘积a´b:
a´b=u´102+(u+w-v)´10+w
现在我们用Karatzuba的诀窍来计算上面的例子:a=78, b=21。Karatzuba的三次乘法如下:
u=7´2 =14,
v=(8-7)´(1-2)=-1,
w=8´1 =8
于是
78´21=14´100+(14+8-(-1))´10+8
=1400+230+8
=1638
借助三个辅助乘积计算最终结果我们需要执行两次基本减法,三次基本乘法在加上几次基本的加法和减法。
四位数的Karatzuba方法
解决了两位数的乘法,现在我们考虑n=4,即两个四位整数a,b相乘的情况。a和b像前面一样拆分为两半,但现在每“一半”不再是个位数,而是两位数了:
a=p´102+q, b=r´102+s
我们像上面一样计算三个辅助乘积:
u=p´r,
v=(q-p)´(s-r),
w=q´s
也如同上面一样,我们可以这三个辅助乘积计算a´b:
a´b=u´104+(u+w-v)´102+w
例: 计算a´b, 其中a=5678, b=4321
首先分别将a,b各拆分为两部分:p=56, q=78,而r=43, s=21。辅助乘积计算如下:
于是
我们需要计算三个两位数的乘积,由前面的分析可知,采用Karatzub方法,每一个需要执行3次基本乘操作,这样我们需要执行3´3=9次基本乘操作加上几个基本加减法操作。如果采用长乘则需要16次基本乘操作以及若干基本加法操作。
Karatzuba方法用于两个任意长的整数相乘
回顾一下前面关于两个四位数相乘的Karatzuba方法,我们知道这可以通过三次两位数相乘来实现,当然两位数相乘时仍然采用Karatzuba方法。沿着同样的思路,很容易理解两个8位数相乘可以用三次4位数相乘实现,而两个16位数相乘可以用三次8位数相乘实现,等等。换言之,Karatzuba方法适用于长度为2的整次幂的任何两个十进制整数相乘,即乘数的位数可以是2=21, 4=2´2=22, 8=2´2´2=23, 16=2´2´2´2=24, 等等。
Karatzuba的一般形式如下:两个n位整数a,b (n=2´2´2´……´2=2k) 拆分为:
它们的乘积a´b按照如下公式计算,使用三次n/2=2k-1 位乘法:
采用Karatzuba方法计算两个2k位整数的乘积只需要计算3次2k-1位乘法,而不是4次。下表比较了在计算两个n位数的乘积时,长乘与Karatzuba方法各自需要的基本操作次数:
位数 Karatzuba 长乘
利用对数很容易将表中的项表示为n的函数:对于n=2k,对应于长乘的列中的值是4k。用log表示以2为底的对数,则k=log(n),于是:
而对于n=2k,对应于Karatzuba方法的列中包含的值为:
我们返回前面讨论的两个100万位的整数相乘的问题。长乘需要执行约4万亿次基本操作,包括1万亿次个位数乘法。而采用Karatsuba方法,首先要在两个乘数前补0,使得其位数成为2的整次幂,对于100万,这个值是220=1048576。这是必须的,否则连续使位数减半不能保证减到一位数。Karatsuba方法“仅仅”执行35亿次基本乘法,只有长乘代价的1/287。你可以想一下,我们常用“五分钟”表示很快,五分钟的1/300是1秒钟。所以说Karatsuba方法确实计算代价小很多,在我们只对基本乘法执行次数计数时尤其如此。精确分析还必须将中间结果的加减运算考虑在内。在乘数位数很少时,长乘实际上更快一些。但是当位数增加,Karatsuba方法的优越就表现出来了,因为它生成的中间结果相对较少。在什么情况下Karatsuba方法一定比长乘快取决于使用的计算机的特性以及大整数在机器中如何表示。
小结
Karatsuba方法之所以有效,背后有两点技巧:
第一个是很通用的原理:“两个n位数相乘”的任务被分解为几个形式相同但规模较小的任务,即“两个n/2位数相乘”。我们连续分解,直到任务成为“一位数相乘”。这样的解题策略称为分治法。本书前面也介绍过这种策略,例如第三章中的快速排序。计算机并不需要为每个分解产生的规模不同的问题单独生成一个程序。我们采用一个带有参数的程序,参数用来表示分解得到的不同任务的规模n。这个程序会用参数值n/2去执行几次,处理分解出来的较小的任务。这称作递归,是计算机中最重要也是最基础的技术之一。
第二个则是特别针对乘法的,就是将规模为n的任务分解为规模为n/2的任务时,设法分解成3个子问题,而不是4个。这个差别看上去不大,但是通过在整个递归链中的积累,效果就非常明显了,这就使得在两个很大的数相乘时(这也意味着递归次数比较多)Karatsuba方法优于长乘。