有一类很经典的题目叫“高精度计算”。我当时特地去另一个城市听过一位计算机教育方面很著名的老师讲了10堂课,其中高精度计算专门用了一天来讲。
当时我们使用的编程语言是Pascal,典型的古典语言,没有什么新特性,没有虚拟机,语法形式比C严格的多,数据类型当然也是很简单的,最普通的integer类型范围为-32768..32767,longint有-2147483648.。2147483647,再高一点的有extended为3.4e-4932..1.1e4932(精度无法保证)。这些数据类型远远不能达到“高精度计算”的要求,比如求解1000位的四则运算,这些基本数据类型就不能表示。
高精度计算在那时候的一般方法是:用其他数据类型来模拟,比如用字符串或字符数组来存储数据,并手工模拟进行运算。
下面给出一题,来自NOIP2003复赛普及组的第4题:
题四、麦森数(Mason.pas)
【问题描述】形如2P-1的素数称为麦森数,这时P一定也是个素数。但反过来不一定,即如果P是个素数,2P-1不一定也是素数。到1998年底,人们已找到了37个麦森数。最大的一个是P=3021377,它有909526位。麦森数有许多重要应用,它与完全数密切相关。
任务:从文件中输入P(1000<P<3100000),计算2P-1的位数和最后500位数字(用十进制高精度数表示)
【输入格式】
文件中只包含一个整数P(1000<P<3100000)
【输出格式】
第一行:十进制高精度数2P-1的位数。
第2-11行:十进制高精度数2P-1的最后500位数字。(每行输出50位,共输出10行,不足500位时高位补0)
不必验证2P-1与P是否为素数。
【输入样例】
1279
【输出样例】
386
00000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000
00000000000000104079321946643990819252403273640855
38615262247266704805319112350403608059673360298012
23944173232418484242161395428100779138356624832346
49081399066056773207629241295093892203457731833496
61583550472959420547689811211693677147548478866962
50138443826029173234888531116082853841658502825560
46662248318909188018470682222031405210266984354887
32958028878050869736186900714720710555703168729087
这就是一道典型的高精度计算,要在平时得马上把加法乘法什么的模拟一下,乘二用位移算更方便什么的,不过这个时代我们已经有了更好的工具,包括一系列先进的动态语言,比如Python就是一种支持大数运算的现代动态语言。
程序如下(程序1):
#!/usr/bin/python import datetime p=(int)(raw_input("Enter a number:")) time1=datetime.datetime.now() r=2**p-1 s=str(r) print len(s) if len(s)>500: s=s[len(s)-500:] elif len(s)<500: s='0'*(500-len(s))+s for i in range(0, 500, 50): print s[i:i+50] time2=datetime.datetime.now() print "time:",time2-time1
测试一下样例中的1279
得到和题目给出的一样的答案,初步是没问题的了。但是题目有个要求"输入P(1000<P<3100000)"那么我们至少要测试一下最大值能否算出,于是我测试一下3099999,然后就出问题了,程序竟然卡在那一动不动,CPU占用率满了。我等了一分钟还没结果,这是怎么回事呢?
原因我想是因为虽然Python支持大数运算,内部也进行了优化,但是计算2**3099999难度还是太大,尤其是计算到后面,位数可能有几十万位,Python并不知道题目只要求最后500位的精度,而是老老实实地全部计算出来了,于是造成了很长时间都没运行完的结果。
知道了原因我们就主动帮他舍弃500位之上的多余的计算就行了,改进程序(程序2)如下:
#!/usr/bin/python import datetime p=(int)(raw_input("Enter a number:")) time1=datetime.datetime.now() r = 1 for i in range(1, p+1): r <<= 1 s=str(r) if len(s)>500: s=s[len(s)-500:] r=int(s) r-=1 s=str(r) print len(s) if len(s)>500: s=s[len(s)-500:] elif len(s)<500: s='0'*(500-len(s))+s time2=datetime.datetime.now() for i in range(0, 500, 50): print s[i:i+50] print "time:",time2-time1
再一次运行并测试输入3099999......奇怪,还是久久不出结果,怎么回事?
改测试输入为10000,在我这台机器上得到结果为:
程序1:0:00:00.010416
程序2:0:00:01.935730
也就是改进后的程序竟然还耗费了差不多19倍的时间完成相同的运算量?
分析两段程序,猜测可能的原因是“舍去500位以上“的操作是分支操作,而CPU进行分支操作是比较慢的,我们每乘一个数就要分支一次大大增加CPU运算量,增加运行时间,所以我们要尽量折中,既要减少数字计算位数,又要减少不必要的判断分支,程序再一次改进,程序3如下:
#!/usr/bin/python import datetime split_work = 1000 p = (int)(raw_input("Enter a number:")) time1 = datetime.datetime.now() l = 0 r = 1 while p > split_work: r *= 2**split_work p -= split_work s = str(r) if len(s) > 500: l += len(s) - 500 s = s[len(s)-500:] r = (int)(s) r *= 2**p r -= 1 s = str(r) print len(s) + l if len(s) > 500: s=s[len(s)-500:] elif len(s) < 500: s = '0'*(500-len(s))+s time2 = datetime.datetime.now() for i in range(0, 500, 50): print s[i: i+50] print "time:", time2-time1
其中split_work参数表示间隔计算2的多少次方后,就进行一次“舍去500位以上”操作,通过调整这个参数可以找到最合适的值能在最短的时间内完成计算。
现在测试3099999:得出了结果
Enter a number:3099999
933000
61051866391707269515429275549722705555937396064823
99913609046259230270064349807496966347459977552460
82272407657633714204046438471194525705920210604262
62056311376319124400803552166408038778860862627241
26178742032383458688465757775553987130543008191635
66873987338757320106113225663868229374409491449205
47777145905869717599995305794743512823748162212567
96039317069451981004402982562865453062267690475507
63944018087226753712185533415250821979816864238682
74699334863010844344279552556742330001816381554687
time: 0:00:01.673990
看上去好像已经完成任务了,实际上答案是错的,错误不明显,那么我们找个明显的来测试。题目说:“到1998年底,人们已找到了37个麦森数。最大的一个是P=3021377,它有909526位。”那么我们测试一下3021377
Enter a number:3021377
909337
11913281261611537667213798436049305566736876178255
88332272350690015415089402574152885277835931459133
40309734813994510763562374502553333760767267082261
94805056498068234364270236322187114005959098576373
86600852826717764565800819358859665607143791528714
49648414600032153277107696032667644008966901945306
68310460272117099806449192863428911515984207543022
30411839060484427823257208111447478189918377204959
69880392336860732039112145134495381589829360634296
37539718233655887458210261770225422631973024694271
time: 0:00:01.613332
发现了吗?得到的位数和题目描述不符合,也就是说我们的位数计算是有错误的。为什么位数会错误呢?我猜想是因为超过500位的部分我们只是简单地舍去,而没有估计到被舍掉的那些位会通过之后的计算对总位数造成的影响,相反,因为500位以上被舍掉了,那么第500位通过之后的计算却有可能使位数增加,如果第500位为5以上的数的话,通过乘以二就可以进位使得总位数增加1。
所以我们要找到新的方法计算2^n-1的位数。
已知log10可以数10进制数的位数,求2^n-1的位数就是log10(2^n-1)+1:
用程序表达就是:
import math p = int(raw_input("Enter a number:")) print int(p * math.log10(2)) + 1
整合到程序3里,我们得到程序4:
#!/usr/bin/python import math import datetime split_work = 1000 p = int(raw_input("Enter a number:")) time1 = datetime.datetime.now() print int(p * math.log10(2)) + 1 r = 1 while p > split_work: r *= 2**split_work p -= split_work s = str(r) if len(s) > 500: s = s[len(s)-500:] r = int(s) r *= 2**p r -= 1 s = str(r) if len(s) > 500: s=s[len(s)-500:] elif len(s) < 500: s = '0'*(500-len(s))+s time2 = datetime.datetime.now() for i in range(0, 500, 50): print s[i: i+50] print "time:", time2-time1
最后运行的结果是:
Enter a number:3021377
909526
11913281261611537667213798436049305566736876178255
88332272350690015415089402574152885277835931459133
40309734813994510763562374502553333760767267082261
94805056498068234364270236322187114005959098576373
86600852826717764565800819358859665607143791528714
49648414600032153277107696032667644008966901945306
68310460272117099806449192863428911515984207543022
30411839060484427823257208111447478189918377204959
69880392336860732039112145134495381589829360634296
37539718233655887458210261770225422631973024694271
time: 0:00:01.633107
还是超过了一秒,可惜。
接下文:高精度计算:梅森数(2)