求圆周率pi的怪异程序 祖冲之圆周率算法绝技之迷


据说华罗庚曾讲过一个故事,说:有个教书先生喜欢喝酒,一天,他叫学生背圆周率,自己却提壶酒到山上庙里找老和尚喝酒去了。有个聪明的学生把圆周率编了个打油诗“山巅一寺一壶酒,尔乐苦煞吾,把酒吃;酒杀尔杀不死,乐尔乐”。其实是3.1415926535897932384626的谐音。先生一回来,学生居然背了下来,可一想,发现学生是在讽刺他。

究其“山巅一寺一壶酒”的来历,众所周之,可上溯至南北朝时的祖冲之(429500),当时获得的结果是圆周率在3.14159263.1415927之间。

那么,祖冲之是怎么算出来的呢?

《隋书·律历志》记载,祖冲之所用的方法叫“缀术”,并说祖冲之“所著之书名为《缀术》,学官莫能究其深奥”。传说唐时此书为朝庭钦定教科书,后来《缀术》失传。从而祖冲之的神奇算法成为千古之迷。

作为猜测,华先生曾说“他的算法也是极限的最好说明,他从单位圆的内接正六边形和外切正六边形出发……再作内接的和外切的正12边形、正24边形……边数愈多内接的和外切的正6·2n1边形的面积就愈接近圆的面积,由此可以逐步地精确地算出圆周的长度。”内接和外切正边形的方法是阿基米德的“穷竭法”。

但仔细一想,用“穷竭法”来算也是有问题的。要得到祖率的精度,需要算正24576边形,也就是从正六边形出发要剖分12次,即同一算法要迭代12次,在用算筹的手工年代要完成如此浩大的计算量其困难是很大的,也可能是很能难实现的。

于是,钱宝琮先生在主编《中国数学史》时猜测采用了与刘徽割圆术相仿的方法,钱先生猜测说:“祖冲之钻研了《九章算术》刘徽注之后,认为数学还应该有所发展,他写成了数十篇专题论文,附缀于刘徽注的后面,叫它‘缀述’。”

“缀”有附着之意,故华中王能超先生同意钱先生的观点,认为“缀术”实际上是《九章算术》刘徽注的“祖冲之注”。但“缀术”决不是“缀述”。

然而,“缀”又有拼合、组合之意,故“缀术”可能是刘徽割圆术的组合之术。

王先生坚信“缀术”是割圆术的组合之术,并由此发现了“缀术”是与当今外推算法相类似的一种加速算法,祖冲之肯定是用这种加速算法减少了迭代次数,解决了计算量大的困难。这种加速算法的智慧是阿基米德穷竭法所不能比的。果真如此的话,王先生就破译了缀术的千古疑案,所以林群先生讲“我认为王教授的发现是数学史上的重大事件”。

那么,该如何“组合”呢?

从圆内接正六边形做起,第一次割得正12边形,用S(12)记其面积,刘徽称其为“觚之幂”,于是得:

S(12)S(24) S(48) S(96)S(192),……

刘徽称多边形的面积为幂,而称偏差

A(n)=S(2n)S(n)

为差幂,并给出了双侧逼近公式,即圆面积介于S(2n)S(2n)A(n)之间,由此给出了一个计算圆面积的近似实例:

由此得到圆周率为3.1416,将此实例写成公式就是:

圆面积≈S(2n)c[S(2n)S(n)]

即是现代的组合加速技术,其关键是系数c的选取。为此,刘徽在割圆术中说了一句人称“十字文”的话:“以十二觚之幂为率消息”,一千多年来让多少行家费尽思量也不解其迷。王先生从刘徽的思想方式与语言行文特征出发,认为是人们传抄之误,实应为“以十二觚之幂率为消息”,意为系数c的消息在十二觚之幂率中。十二觚之幂率为

D(12)A(12)/A(24)[S(24)S(12)]/[S(48)S(24)]3.95

可得系数c1/(3.951)1/2.95,这是刘徽的伟大之处,也是刘徽与3.1415926失之交臂而把更精致的结果留给了200年后的老祖的原因。如果把

D(12)D(24)D(48)D(96)

都算出来,就会发现它们稳定在4.0上,从而系数c1/3。如果取c1/3,只需算到正96边形的S(96)192边形的S(192),再利用加速公式就可以得到3.141592646,这时只需要迭代5次而不是12次,也不需要算到正24576边形,大概这就是老祖的绝技之迷。

公元前300年,阿基米德为得到3.14,已经割到正96边形了,如果他老先生知道如何跑得快一点就能喝到一壶酒了,没有离“山巅一寺一壶酒”的一步之遥了。

这一绝技在现代已不重要了,在大学本科的计算方法中只是很简单的外推加速技术。所以,著名的克莱因在《古今数学思想》自叙称:“为着不使资料漫无边际,我忽略了几种文化,例如中国的文化,因为他们的工作对于数学思想的主流没有重大的影响。”

如果再问一个问题:老刘和老祖在那个时代是怎么想到加速与c1/3的?

一个比较靠得住的猜想是,他们是从数字的分析中直觉地感悟到的,在逻辑上不是从分析逻辑上得到的,而是从直觉逻辑上得到的。有人把这类直觉、感悟叫做智慧。擅长数学主流分析逻辑的西方数学家伊恩·斯图尔特说:“数学的全部力量就在于直觉和严格性巧妙地结合在一起。受控制的精神和富有灵感的逻辑正是数学的魅力所在,也是数学教育者努力的方向。”他更是一语道破了数学的真谛:“直觉是真正的数学家赖以生存的东西”。

无独有偶,迪瓦多内也说:“这些富有创造性的科学家与众不同的地方,在于他们对研究的对象有一个活生生的构想和深刻的了解,这些构想和了解结合起来,就是所谓‘直觉’”。

也许对数学主流思想产生重大影响的恰好是人类的直觉思维,这才是今天人们仍然尊敬刘徽与祖冲之的原因。

如何让数学家经常带点直觉的灵感呢,德国数学家维尔斯特拉斯说:“不带点诗人味的数学家,绝不是一个完美的数学家。”

所以,一个完美而又简单的修练方法就是到学人亭来。

 

2004年,获王能超先生慧赠《千古绝技“割圆术”——刘徽的大智慧》,谨以此文答谢。

 

引言
   网上流传着一个怪异的求pi程序,虽然只有三行却能求出pi值连小数点前共800位。这个程序如下:

/*某年Obfuscated C Contest佳作选录:*/ 
#include < stdio.h>
long a=10000, b, c=2800, d, e, f[2801], g; 
main(){
for(;b-c;)f[b++]=a/5; 
for(;d=0,g=c*2;c-=14,printf("%.4d",e+d/a),e=d%a) 
for(b=c;d+=f[b]*a,f[b]=d%--g,d/=g--,--b;d*=b);


/* (本程式可算出pi值连小数点前共800位) 
(本程式录自sci.math FAQ,原作者未详)*/ 

咋一看,这程序还挺吓人的。别慌,下面就告诉你它是如何做到的,并且告诉你写怪异C程序的一些技巧。^_^

展开化简
   我们知道,在C语言中,for循环和while循环可以互相代替。

   for(statement1;statement2;statement3){
     statements;
   }

上面的for语句可以用下面的while语句来代替:

   statement1;
   while(statement2){
     statements;
     statement3;
   }

而且要写怪异的C程序,逗号运算符无疑是一个好的助手,它的作用是: 
从左到右依次计算各个表达式的值,并且返回最右边表达式的值。
把它嵌入for循环中是写怪异代码的常用技巧之一。所以,上面的程序可以展开为: 
#include < stdio.h> /*1*/
/*2*/
long a=10000, b, c=2800, d, e, f[2801], g; /*3*/
main(){ /*4*/
   while(b-c!=0){ /*5*/
     f[b]=a/5; /*6*/
     b++; /*7*/
     } /*8*/
   d=0; /*9*/
   g=c*2; /*10*/
   while(g!=0){ /*11*/
     b=c; /*12*/
     d+=f[b]*a; /*13*/
     f[b]=d%--g; /*14*/
     d=d/g--; /*15*/
     --b; /*16*/
     while(b!=0){ /*17*/
         d=d*b+f[b]*a; /*18*/
         f[b]=d%--g; /*19*/
         d=d/g--; /*20*/
         --b; /*21*/
         } /*22*/
     c-=14; /*23*/
     printf("%.4d",e+d/a); /*24*/
     e=d%a; /*25*/
     d=0; /*26*/
     g=c*2; /*27*/
     } /*28*/
   } /*29*/

现在是不是好看一点了?

进一步化简
   你应该能注意到a的值始终是10000,所以我们可以把a都换成10000。再就是,仔细观察g,在外层循环中,每次循环用它做除法或取余时,它总是等于2*c-1,而b总是初始化为c。在内层循环中,b每次减少1,g每次减少2。你这时会想到了吧?用2*b-1代替g!代进去试试,没错!另外,我们还能做一点化简,第26行的d=0是多余的,我们把它合并到第13行中去,第13行可改写为: d=f[b]*a; 。所以程序可以改为: 
#include < stdio.h>

long b, c=2800, d, e, f[2801]; 
main(){
   while(b-c!=0){
     f[b]=2000;
     b++;
     }

   while(c!=0){
     b=c;
     d=f[b]*10000;
     f[b]=d%(b*2-1);
     d=d/(b*2-1);
     --b;
     while(b!=0){
         d=d*b+f[b]*10000;
         f[b]=d%(b*2-1);
         d=d/(b*2-1);
         --b;
         }
     c-=14;
     printf("%.4d",e+d/10000);
     e=d%10000;
     }
   } 

少了两个变量了!

深入分析
   好了,马上进入实质性的分析了。一定的数学知识是非常有必要的。首先,必须知道下面的公式可以用来求pi:
pi/2=1+1!/3!!+2!/5!!+3!/7!!+...+k!/(2*k+1)!!+...
只要项数足够多,pi就有足够的精度。至于为什么,我们留给数学家们来解决。
   写过高精度除法程序的人都知道如何用整数数组来进行除法用法,而避免小数。其实很简单,回想一下你是如何徒手做除法的。用除数除以被除数,把得数放入结果中,余数乘以10后继续做下一步除法,直到余数是零或达到了要求的位数。
   原程序使用的数学知识就那么多,之所以复杂难懂是因为它把算法非常巧妙地放到循环中去了。我们开始具体来分析程序。首先,我们从数学公式开始下手。我们求的是pi,而公式给出的是pi/2。所以,我们把公式两边同时乘以2得:

   pi=2*1+2*1!/3!!+2*2!/5!!+2*3!/7!!+...+2*k!/(2*k+1)!!+...

接着,我们把它改写成另一种形式,并展开:

   pi=2*1+2*1!/3!!+2*2!/5!!+2*3!/7!!+...+2*n!/(2*n+1)!!

   =2*(n-1)/(2*n-1)*(n-2)/(2*n-3)*(n-3)/(2*n-5)*...*3/7*2/5*1/3
             +2*(n-2)/(2*n-3)*(n-3)/(2*n-5)*...*3/7*2/5*1/3
                       +2*(n-3)/(2*n-5)*...*3/7*2/5*1/3
                                   +2*3/7*2/5*1/3
                                     +2*2/5*1/3
                                       +2*1/3
                                         +2*1
对着公式看看程序,可以看出,b对应公式中的n,2*b-1对应2*n-1。b是从2800开始的,也就是说n=2800。(至于为什么n=2800时,能保证pi的前800位准确不在此讨论范围。)看程序中的相应部分: 
d=d*b+f[b]*10000;
f[b]=d%(b*2-1);
d=d/(b*2-1);

d用来存放除法结果的整数部分,它是累加的,所以最后的d将是我们要的整数部分。而f[b]用来存放计算到b为止的余数部分。
   到这里你可能还不明白。一是,为什么数组有2801个元素?很简单,因为作者想利用f[1]~f[2800],而C语言的数组下标又是从0开始的,f[0]是用不到的。二是,为什么要把数组元素除了f[2800]都初始化为2000?10000有什么作用?这倒很有意思。因为从printf("%.4d",e+d/10000); 看出d/10000是取d的第4位以前的数字,而e=d%10000; ,e是d的后4位数字。而且,e和d差着一次循环。所以打印的结果恰好就是我们想要的pi的相应的某4位!开始时之所以把数组元素初始化为2000,是因为把pi放大1000倍才能保证整数部分有4位,而那个2就是我们公式中两边乘的2!所以是2000!注意,余数也要相应乘以10000而不是10!f[2800]之所以要为0是因为第一次乘的是n-1也就是2799而不是2800!每计算出4位,c都要相应减去 14,也就保证了一共能够打印出4*2800/14=800位。但是,这竟然不会影响结果的准确度!本人数学功底不高,无法给出确切答案。(要是哪位达人知道,请发email到xiyou.wangcong@gmail.com告诉我哦。)

   偶然在网上见到一个根据上面的程序改写的“准确”(这个准确是指没有漏掉f[]数组中的的任何一个元素。)打印2800位的程序,如下: 
long b,c=2800,d,e,f[2801],g;
int main(int argc,char* argv[])
{
   for(b=0;b        f[b] = 2;
   e=0;
   while(c > 0)
   {
     d=0;
     for(b=c;b>0;b--)
     {
         d*=b;
         d+=f[b]*10;
         f[b]=d%(b*2-1);
         d/=(b*2-1);
     }
     c-=1;
     printf("%d",(e+d/10)%10);
     e=d%10;
   }
   return 0;


不妨试试把上面的程序压缩成3行。
结论
   以Knuth图灵演讲中的一句话结束全文: 
We have seen that computer programming is an art, because it applies accumulated knowledge to the world, because it requires skill and ingenuity, and especially because it produces objects of beauty. A programmer who subconsciously views himself as an artist will enjoy what he does and will do it better. 

与大家共勉!^_^ 



 

 

 
 

 

 
 


















 

 

































 


 






















 























 







 

















posted @ 2010-03-23 23:21  Alamps  阅读(1865)  评论(1编辑  收藏  举报