大數階乘算法

一:精度要求較低的階乘算法

如果只是要求算法的速度,而對精度要求比較低的話可以直接使用,斯特林公式計算n!

斯特林公式如下:

n!=sqrt(2*PI*n)*(n/e)^n*(1+1/12/n+1/288/n2–139/51840/n3-571/2488320/n4+…)

ln(n!)=0.5*ln(2*PI)+(n+0.5)*ln(n)-n+(1/12/n -1/360/n3+ 1/1260/n5)-…,

這裏PI爲圓周率,而最後一頊爲雅格布·伯努力數是無窮的級數,這裏我們取前5項即可得到接近16位有效數字的近似值,而精度的提高可由雅格布·伯努力數取的項數增加而得到。

另也可對斯特林公式進行修行,下面即爲蔡永裕先生的《談Stirling公式的改良》一文中得出的斯特林公式的修正版:

n! = n^n*sqrt(2*n*PI)/exp(n*(1-1/12sqrt(n)+0.4))

該公式可以得到較高的精度,較計算也較方便。


二:高精度階乘算法

算法1:硬乘

最容易想到的自然是硬乘。模擬人工計算乘法的方法,一位位的乘。以AB*C爲例,其中A,B,C各佔一個基數位,以N進製爲例。

第一步:

Y = B*C

看Y是否大於等於N,如果是的話進位。設Cn爲進位量,即Cn =Y/N,而本位Y=Y%N

第二步:

X = A*C+Cn

看X是否大於等於N,如果是的話進位。而本位X=X%N

在這個過程中可以用數組來存放大數的每一個位,爲了提高效率可以使用long型來存放大數的每一位,同時改10進製爲100000,當然也可以更大一些但最好基數仍爲了10的幕數,這樣便於輸出,當然取的基數更大些可以更好的發揮long型的效能,但是注意不要讓X*X>=LONG_MAX,即基數最大隻可爲sqrt(LONG_MAX)。

第三步:

然後通過

      n! = n*(n-1)*(n-2)*(n-3)……3*2

得出結果,該方法計算10000!大概需3-4秒(CY1.7G ,512內存)

用一這種方法,效率並不好,假如已經求得n!(共有m個單元),計算(n+1)!需要 m次乘法,m次加法(加法速度較快,可以不予考慮,下同),m次求餘(求本位),m次除法(求進位),結果爲m+1的單元計算(n+2)!需要m+1次乘法,m+1次求餘,m+1次除法,結果爲m+1個單元計算(n+3)!需要m+1次乘法,m+1次求餘m+1次除法,結果爲m+2個單元。 
      計算(n+4)!   需要   m+2次乘法,  m+2次求餘   ,m+2次除法  ,結果爲m+2個單元。  
      計算(n+5)!   需要   m+2次乘法,  m+2次求餘   ,m+2次除法  ,結果爲m+3個單元。  
     計算(n+6)!  ...  
     計算(n+7)!  ...  
     計算(n+8)!  ...  
     計算(n+9)!  ...  
     計算(n+10)!  ...  
     計算(n+11)!  ...  
     計算(n+12)!  ...  
     計算(n+13)!  ...  
      計算(n+14)!   需要  m+7次乘法,m+7次求餘   ,m+7次除法  ,結果爲m+7個單元  
     計算(n+15)!  需要   m+7次乘法,m+7次求餘  ,m+7次除法   ,結果爲m+8個單元 
     計算(n+16)!  需要   m+8次乘法,m+8次求餘  ,m+8次除法   ,結果爲m+8個單元 

由此可估計該算法的複雜度約爲:T(n) =O(n!),是一個NP難問題。   


算法2:分治法  

1  總體思想

假如已經求得n!(共有m個單元),求(n+16)!

 第一步:

將n+1   與n+2   相乘,將n+3  與n+4   相乘,將n+5 與n+6...n+15與n+16,得到8個數,叫做n1,n2,n3,n4,n5,n6,n7,n8。

第二步:

n1與n2相乘,結果叫p2,結果爲2個單元,需要1次乘法,1次除法,1次求餘。

n3與n4相乘,結果叫p4,結果爲2個單元,需要1次乘法,1次除法,1次求餘。

n5與n6相乘,結果叫p6,結果爲2個單元,需要1次乘法,1次除法,1次求餘。

n7與n8相乘,結果叫p8,結果爲2個單元,需要1次乘法,1次除法,1次求餘。

第三步:

p2與p4相乘,結果叫q4,結果爲4個單元,需要4次乘法,4次除法,4次求餘,2次加法。

p6與p8相乘,結果叫q8,結果爲4個單元,需要4次乘法,4次除法,4次求餘,2次法。

第四步:

p6與p8相乘,結果叫f8,結果爲8個單元,需要8次乘法,8次除法,8次求餘,4次加法。這一過程的複雜度爲20次乘法,20次除法,20次求餘。

假定求餘運算和除法運算和乘法的複雜度相同,則可知其符合分治法所需時間的計算公式,故可得:   T(n) = log(n^2)

因數學水平及時間有限不能給出算法1和算法2的精確 算法複雜度,只能給出算法1和算法2之間大致的複雜度。     

第二種算法表明,在計算階乘時,通常的方法(先計算出n的階乘,再用一位數乘以多位數的方法計算(n+1)的階乘,再計算n+2的階乘)不是最優的,更優化的算法是,計算出相鄰的幾個數的積(稱之爲部分積),用部分積乘以部分積的這種多位數乘以 多位數的算法來計算。並且根據平衡子問題的思想:在用分治法設計算法時,最好使子問題的規模大致相同。即在計算過程中爲提高效率可在兩相乘時,兩個乘法的長度最爲接近的優先進行。爲此可以根據乘數的長度構造小根堆,將最短乘數和次短乘法相乘,並將結果插入小根堆中,繼續運算,直到得到結果。


2  兩個數相乘

設X和Y都是n(n是偶數)位的L進制的整數(當X,Y位數不等時,可在較小的數的左邊補零來使其位數和較大的數相等。如X=123,Y=4325,則令X=0123)。在第一種算法中,兩個大數相乘採用的是硬乘。效率較低,如果將每兩個一位數的乘法或加法看作一步運算的話,那麼這種方法要作O(n^2)步運算才能求出乘積XY。

這裏我們用二分法來計算大數乘法。將n位的L進制的X和Y都分爲2段,每段的長爲n/2(如n爲奇數可在左邊加零),如下圖如示:


由此,X=A*L^(n/2)+B         Y=C*L^(n/2)+D

所以 X*Y = (A*L^(n/2)+B)(C*L^(n/2)+D)   = AC*L^n+(AD+BC)*L^(n/2)+BD

而AD+BC = (A-B)(D-C)+AC +BD

所以 X*Y= AC*L^n +((A-B)(D-C)+AC +BD)*L^(n/2)+BD

此時,此式僅需做3次n/2位整數的乘法(AC,BD和(A-B)(D-C)),6次加、減法和2次移位。由此可得:

T(n) = O(1)                 n= 1時

T(n) = 3T(n/2)+O(n)    n>1時

根據分治法效率計算公式可得:T(n)= O(n^log3) =O(n^1.59)


同理,若將n三等分,則

(Axx+Bx+C)*(Dxx+Ex+F)=ADxx+(AE+BD)xxx+(AF+BE+CD)xx+(BF+CE)x+CF

=ADxxxx+[(A-B)(E-D)+AD+BE]xxx+[BE+(A-C)(F-D)+CF+AD]xx+[(B-C)(F-E)+CF+BE]+CF

可知此式僅需做6次n/3位整數的乘法,

根據分治法效率計算公式可得:T(n)= O(n^log6/3) =O(n^2)

取自:http://blog.sina.com.cn/s/blog_40a3dcc1010008nf.html

相關實現:

#include<iostream>
#define MAX 1000
using namespace std;

int main(void)
{
    int n;
    while(scanf("%d",&n)==1&&n>=0)
    {
        int i,j;
        int a[MAX];      //存數運算結果
        int p,h;           //p存儲當前結果的位數,h爲進位
        a[1]=1;
        p=1;  
        for(i=2;i<=n;i++)   //循環與2,3,4.....n相乘
        {
            for(j=1,h=0;j<=p;j++)    //讓a[]的每位與i相乘
            {
                a[j]=a[j]*i+h;
                h=a[j]/10;
                a[j]=a[j]%10;
            }
            while(h>0)         //如果h不爲0
            {
                a[j]=h%10;
                h=h/10;
                j++;
            }
            p=j-1;            //將當前的位數賦給p
        }
        for(i=p;i>=2;i--)
        {
            printf("%d",a[i]);
        }
        printf("%d\n",a[i]);
    }
    return 0;
}

取自:http://www.cnblogs.com/dolphin0520/archive/2011/07/16/2108006.html


代碼解讀:

首先,定義兩個整型的數組:
int fac[1000];//暫且先設定是1000位,我稱之爲“結果數組”
int add[1000];//我稱之爲“進位數組”

現在具體說明兩個數組的作用:
1.fac[1000]
比如說,一個數5的階乘是120,那麼我就用這個數組存儲它:
fac[0]=0
fac[1]=2
fac[2]=1
現在明白了數組fac的作用了吧。用這樣的數組我們可以放階乘後結果是1000位的數。

2.在介紹add[1000]之前,我介紹一下算法的思想,就以6!爲例:
 從上面我們知道了5!是怎樣存儲的。
 就在5!的基礎上來計算6!,演示如下:
fac[0]=fac[0]*6=0
fac[1]=fac[1]*6=12
fac[2]=fac[2]*6=6

3.現在就用到了我們的:“進位數組”add[1000].
 先得說明一下:add就是在第2步中用算出的結果中,第i位向第i+1位的進位數值。還是接上例:
add[0]=0;
add[1]=1;  // 計算過程:就是 (fac[1]+add[0])  %  10=1
add[2]=0;  // 計算過程:就是 (fac[2]+add[1]) % 10=0
.......
.......

add[i+1] =( fac[i+1] + add ) % 10

現在就知道了我們的數組add[]是幹什麼的了吧,並且明白了add是如何求得的了吧。


4.知道了add[1000]的值,現在就在 1. 和 3 . 兩步的基礎上來計算最終的結果。(第2步僅作爲我們理解的步驟,我們下面的計算已經包含了它)

fac[0] = ( fac[0]*6 )  mod 10 =0 
fac[1] = ( fac[1]*6 + add[0] ) mod 10 =2
fac[2] = ( fac[2]*6 + add[1] ) mod 10=7

到這裏我們已經計算完了6!。然後就可以將數組fac[1000]中的數,以字符的形式按位輸出到屏幕上了。

5.還有一點需要說明,就是我們需要一個變量來記錄fac[1000]中實際用到了幾位,比如5!用了前3位;
我們在這裏定義爲 top 
爲了計算top,我們有用到了add[1000],還是以上面的爲例:
 5!時,top=3,在此基礎上我們來看6!時top=?
由於  add[2]=0      所以  top=3(沒有變)
也就是說,如果最高位有進位時,我們的top=top+1,否則,top值不變。


6.總結一下,可以發現,我們把階乘轉化爲 兩個10以內的數的乘法,還有兩個10以內的數的加法了。 因此,不管再大的數,基本上都能算出了,只要你的數組夠大就行了。

取自:http://blog.csdn.net/jaylongli/article/details/4865035

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章