外星人計算Pi的程序

一、源程序
本文分析下面這個很流行的計算PI的小程序。下面這個程序初看起來似乎摸不到頭腦,
不過不用擔心,當你讀完本文的時候就能夠基本讀懂它了。
程序一:很牛的計算Pi的程序
int 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,這個程序所用的公式如下:
          1          2          3                    k
pi = 2 + --- * (2 + --- * (2 + --- * (2 + ...  (2 + ---- * (2 + ... ))...)))

          3          5          7                   2k+1
至於這個公式爲什麼能夠計算出PI,已經超出了本文的能力範圍。
下面要做的事情就是要分析清楚程序是如何實現這個公式的。
我們先來驗證一下這個公式:
程序二:Pi公式驗證程序
#include "stdio.h"
void main()
{
   float pi=2;
   int  i;
   for(i=100;i>=1;i--)
      pi=pi*(float)i/(2*i+1)+2;
   printf("%f\n",pi);
   getchar();
}
上面這個程序的結果是3.141593。
三、程序展開
在正式分析程序之前,我們需要對程序一進行一下展開。我們可以看出程序一都是使用
for循環來完成計算的,這樣做雖然可以使得程序短小,但是卻很難讀懂。根據for循環
的運行順序,我們可以把它展開爲如下while循環的程序:
程序三:for轉換爲while之後的程序
int a=10000,b,c=2800,d,e,f[2801],g;
main() {
 int i;
 for(i=0;i<c;i++)
     f[i]=a/5;
 while(c!=0)
     {
         d=0;
         g=c*2;
         b=c;
         while(1)
            {
                d=d+f[b]*a;
                g--;
                f[b]=d%g;
                d=d/g;
                g--;
                b--;
                if(b==0) break;
                d=d*b;
            }
         c=c-14;
         printf("%.4d",e+d/a);
         e=d%a;
    }
}
注:
for([1];[2];[3]) {[4];}
的運行順序是[1],[2],[4],[3]。如果有逗號操作符,例如:d=0,g=c*2,則先運行d=0,
然後運行g=c*2,並且最終的結果是最後一個表達式的值,也就是這裏的c*2。
下面我們就針對展開後的程序來分析。
四、程序分析
要想計算出無限精度的PI,我們需要上述的迭代公式運行無數次,並且其中每個分數也
是完全精確的,這在計算機中自然是無法實現的。那麼基本實現思想就是迭代足夠多次
,並且每個分數也足夠精確,這樣就能夠計算出PI的前n位來。上面這個程序計算800位
,迭代公式一共迭代2800次。
int a=10000,b,c=2800,d,e,f[2801],g;
這句話中的2800就是迭代次數。
由於float或者double的精度遠遠不夠,因此程序中使用整數類型(實際是長整型),分
段運算(每次計算4位)。我們可以看到輸出語句 printf("%.4d",e+d/a); 其中%.4就是
把計算出來的4位輸出,我們看到c每次減少14( c=c-14;),而c的初始大小爲2800,因
此一共就分了200段運算,並且每次輸出4位,所以一共輸出了800位。
由於使用整型數運算,因此有必要乘上一個係數,在這個程序中係數爲1000,也就是說
,公式如下:
               1          2          3                    k
1000*pi = 2k+ --- * (2k+ --- * (2k+ --- * (2k+ ...  (2k+ ---- * (2k+ ... )).
..)))
               3          5          7                   2k+1
這裏的2k表示2000,也就是f[2801]數組初始化以後的數據,a=10000,a/5=2000,所以下面
的程序把f中的每個元素都賦值爲2000:
 for(i=0;i<c;i++)
     f[i]=a/5;
你可能會覺得奇怪,爲什麼這裏要把一個常數儲存到數組中去,請繼續往下看。
我們先來跟蹤一下程序的運行:
 while(c!=0)  假設這是第一次運行,c=2800,爲迭代次數
     {
         d=0;
         g=c*2;  這裏的g是用來做k/(2k+1)中的分子
         b=c;    這裏的b是用來做k/(2k+1)中的分子
         while(1)
            {
                d=d+f[b]*a; f中的所有的值都爲2000,這裏在計算時又把係數擴大了
a=10000倍。

                                            這樣做的目的稍候介紹,你可以看到
輸出的時候是d/a,所以這不影
                        計算
                g--;
                f[b]=d%g; 先不管這一行
                d=d/g;   第一次運行的g爲2*2799+1,你可以看到g做了分母
                g--;
                b--;
                if(b==0) break;
                d=d*b; 這裏的b爲2799,可以看到d做了分子。
            }
         c=c-14;
         printf("%.4d",e+d/a);
         e=d%a;
    }
只需要粗略的看看上面的程序,我們就大概知道它的確是使用的那個迭代公式來計算Pi
的了,不過不知道到現在爲止你是否明白了f數組的用處。如果沒有明白,請繼續閱讀。

d=d/g,這一行的目的是除以2k+1,我們知道之所以程序無法精確計算的原因就是這個除
法。即使用浮點數,答案也是不夠精確的,因此直接用來計算800位的Pi是不可能的。那
麼不精確的成分在哪裏?很明顯:就是那個餘數d%g。程序用f數組把這個誤差儲存起來
,再下次計算的時候使用。現在你也應該知道爲什麼d=d+f[b]*a;中間需要乘上a了吧。
把分子擴大之後,纔好把誤差精確的算出來。
d如果不乘10000這個係數,則其值爲2000,那麼運行d=d/g;則是2000/(2*2799+1),這
種整數的除法答案爲0,根本無法迭代下去了。
現在我們知道程序就是把餘數儲存起來,作爲下次迭代的時候的參數,那麼爲什麼這麼
做就可以使得下次迭代出來的結果爲
接下來的4位數呢?
這實際上和我們在紙上作除法很類似:
       0142
    /——------
 7 /   1
       10
        7
---------------
        30
    28
---------------
         20
         14
---------------
          60
     .....
我們可以發現,在做除法的時候,我們通常把餘數擴大之後再來計算,f中既然儲存的是
餘數,而f[b]*a;則正好把這個餘數擴大了a倍,然後如此循環下去,可以計算到任意精
度。
這裏要說明的是,事實上每次計算出來的d並不一定只有4位數,例如第一次計算的時候
,d的值爲31415926,輸出4位時候,把低四位的值儲存在e中間,e=d%a,也就是5926。

最後,這個c=c-14不太好理解。事實上沒有這條語句,程序計算出來的仍然正確。只是
因爲如果迭代2800次,無論分數如何精確,最後Pi的精度只能夠達到800。
你可以把程序改爲如下形式嘗試一下:
 for(i=0;i<800;i++)
     {
         d=0;
         g=c*2;
         b=c;
         while(1)
            {
                d=d+f[b]*a;
                g--;
                f[b]=d%g;
                d=d/g;
                g--;
                b--;
                if(b==0) break;
                d=d*b;
            }
        // c=c-14; 不要這句話。
         printf("%.4d",e+d/a);
         e=d%a;
    }
最後的答案仍然正確。
不過我們可以看到內循環的次數是c次,也就是說每次迭代計算c次。而每次計算後續位
數的時候,迭代次數減少14,而不影響精度。爲什麼會這樣,我沒有研究。另外最後的
e+d/a,和e=d/a的作用就由讀者自己考慮吧。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章