1. 程式人生 > >求大數n,m下組合數C(n+m,m)%Mod

求大數n,m下組合數C(n+m,m)%Mod

原題是機器人走方格的問題:M * N的方格,一個機器人從左上走到右下,只能向右或向下走。有多少種不同的走法?由於方法數量可能很大,只需要輸出Mod 10^9 + 7的結果。
此問題很簡單,就直接是C(M+N-2,M-1)即可,但是當M+N很大時,是無法直接求出C(M+N-2,M-1)的,所以專門總結了一下大數下組合數的求解方法。

求大數的階乘,如果不精確的話可以用斯特林公式:
斯特林公式
1、轉換為對數式,lg(C(n+m,m))=lg((n+m)!/(n!*m!))=lg((n+m)!)-lg(n!)-lg(m!)=lg((m+1)(m+2)…(m+n))-lg(n!),將乘積化為加法運算。

//  對數法求C(n+m,m)
#include <iostream> #include <cmath> using namespace std; typedef long long ll; #define Mod 1000000009 ll Cnm(int n, int m) { double sum1=0, sum2=0; ll res=(ll)1; for(int i=1; i<=m; i++) { sum1+=(double)log(i); sum2+=(double)log(n-m+i); } return (ll)exp
(sum2-sum1)%Mod; } int main() { int n,m; cin >> n >> m; cout << Cnm(n+m,(m>n?n:m)) << endl; system("pause"); return 0; }

但是此方法依然不適用與n、m過大的情況,但比直接計算階乘適用範圍要廣得多。
2、同樣不適用n過大情況的大數組合計算方法還有利用楊輝三角公式求解,C(n+m,m)=C(n+m-1,m-1)+C(n+m-1,m),此公式證明很簡單,展開拆分一項即可。

//  楊輝三角公式求C(n+m,m)
#include <iostream> #include <vector> using namespace std; #define Mod 1000000009 typedef long long ll; ll Cnm(int n, int m) { vector<vector<ll>> Combi(n+1,vector<ll>(m+1,(ll)0)); for(int i=0; i<(int)Combi[0].size(); i++) Combi[0][i]=(ll)0; for(int i=0; i<(int)Combi.size(); i++) Combi[i][0]=(ll)1; for(int i=1; i<=n; i++) { for(int j=1; j<=(i>m?m:i); j++) { Combi[i][j]=(Combi[i-1][j-1]+Combi[i-1][j])%Mod; } } return Combi.back().back(); } int main() { int n,m; cin >> n >> m; cout << Cnm(n+m,m) << endl; system("pause"); return 0; }

3、利用n!=2^a1*3^a2*…p^ak(p為質數),將原方程(n+m)!/(n!*m!)化為2^(a1-b1-c1)*3^(a2-b2-c2)…*p^(ak-bk-ck),n!分解後p的次數為:n/p+n/p^2+…+n/p^k,則原式可以化成這k項式子對Mod取餘的乘積。

//  質數化簡求C(n+m,m)
#include <iostream>
#include <vector>
using namespace std;
#define Mod 1000000009
typedef long long ll;
//  計算n以內所有的質數
vector<int> primelessthanN(int n)
{
    vector<bool> isprime(n+1, true);
    vector<int> prime;
    prime.push_back(2);
    int i;
    for(i=3; i*i<=n; i+=2)
    {
        if(isprime[i])
        {
            prime.push_back(i);
            for(int j=i*i; j<=n; j+=i)
                isprime[j]=false;
        }
    }
    while(i<=n)
    {
        if(isprime[i])
            prime.push_back(i);
        i+=2;
    }
    return prime;
}
//  計算素數因子p的指數
int Cal(int n, int p)
{
    int res=0;
    ll rec=p;
    while((ll)n>=rec)
    {
        res+=(int)((ll)n/rec);
        rec*=(ll)p;
    }
    return res;
}
//  計算n^k對Mod取餘
ll PowerMod(int n, int k)
{
    int t(k),n0(n);
    ll res=(ll)1;
    while(t)
    {
        if(t&1)
            res=(res*(ll)n0)%Mod;
        n0=(n0*n0)%Mod;
        t>>=1;
    }
    return res;
}
//  計算C(n,m),即原式的C(n+m,m)
ll Cnm(int n, int m)
{
    vector<int> prime=primelessthanN(n);
    ll res=1;
    for(int i=0; i<prime.size(); i++)
    {
        res=(res*(PowerMod(prime[i],Cal(n,prime[i])-Cal(n-m,prime[i])-Cal(m,prime[i]))))%Mod;
    }
    return res;
}
//  main函式
int main()
{
    int n,m;
    cin >> n >> m;
    cout << Cnm(n+m,(m>n?n:m)) << endl;
    system("pause");
    return 0;
}

4、Lucas定理,p是一個素數,將n、m均轉化為p進位制數,表示如下:
(適用範圍:n和m比較大,p是素數且比較小的時候)
m=m0+m1*p+m2*p^2+…mk*p^k,n=n0+n1*p+n2*p^2+…+nk*p^k,則C(n,m)=C(n0,m0)*C(n1,m1)*C(n2,m2)…*C(nk,mk)%p,即Lucas(n,m,p)=C(n%p,m%p)*Lucas(n/p,m/p,p)。
證明方法是利用p-進位制的唯一性,具體證明如下Lucas定理

//  Lucas定理求C(n+m,m)
#include <iostream>
#include <vector>
using namespace std;
#define Mod 10009
typedef long long ll;
vector<long long> ff(Mod+1,1); 
//  預處理階乘陣列Combi
void InitFF(int mod)
{
    for(int i=1; i<=mod; i++)
        ff[i]=(ff[i-1]*i)%mod;
}
//  求最大公因數
int gcd(int a, int b)
{
    if(b==0) return a;
    return gcd(b,a%b);
}
//  階線性同餘方程,利用歐幾里得定理,即求組合數分母階乘式的
//  乘法逆元,具體看程式碼。
void _gcd(int a, int b, ll& x, ll& y)
{
    if(b==0)
    {
        x=(ll)1;
        y=(ll)0;
    }
    else
    {
        _gcd(b,a%b,x,y);
        ll temp=x;
        x=y;
        y=temp-(a/b)*y; //  x,y分別為ax+by=1的解,此方程的x滿足(a*x)%y=1,即x為a的乘法逆元
        //  其實由費馬小定理可知a的乘法逆元是a^(y-2)
    }
}
//  計算小n(在Mod範圍內)時的組合數
ll C(int n, int m)
{
    if(n<m) return 0;
    ll res=(ll)1, x, y;
    int b=(int)(ff[n-m]*ff[m])%Mod;
    int a=(int)ff[n];
    int c=gcd(a,b);
    a/=c;
    b/=c;   //  保證a、b互質
    _gcd(b, Mod, x, y);
    x=(x+Mod)%Mod;      //  防止x為負數
    res=(x*a)%Mod;      //  求出C(n,m)
    return res;
}
//  Lucas函式
ll Cnm(int n, int m)
{
    InitFF(Mod);
    ll res=(ll)1;
    int m0(m), n0(n);
    while(m0||n0)
    {
        res=(res*C(n0%Mod, m0%Mod))%Mod;
        n0/=Mod;
        m0/=Mod;
    }
    return res; 
}

int main()
{
    int n,m;
    cin >> n >> m;
    cout << Cnm(n+m,(m>n?n:m)) << endl;
    system("pause");
    return 0;
}

這裡簡單介紹一下擴充套件歐幾里得演算法,是利用ax+by=c,其中c%gcd(a,b)=0,來求解滿足條件的x,y值,其中x就是a對模b的乘法逆元,而y就是b對模a的乘法逆元,詳解請參考http://blog.csdn.net/qq_27599517/article/details/50888092
5、利用乘法逆元求解
此方法其實與上一種相同,求解乘法逆元除了用擴充套件歐幾里得演算法外,還可以直接由費馬小定理得到(若對於a,p存在x,使得a*x mod p=1,那麼我們稱x為a對p的乘法逆元),而費馬小定理是:假如p是質數,且Gcd(a,p)=1,那麼 a(p-1)(mod p)≡1。即:假如a是整數,p是質數,且a,p互質(即兩者只有一個公約數1),那麼a的(p-1)次方除以p的餘數恆等於1。
證明:
假如inv是b對於p的乘法逆元,即b*inv=p*t+1(t為整數),移項得inv=(p*t+1)/b
(a*inv) mod p
=(a*((p*t+1)/b)) mod p
=(a*(p*t/b+1/b)) mod p
=(a/b) mod p+(a*p*t/b) mod p
∵ (a*p*t/b) mod p=0
∴ 原式=(a/b) mod p
即(a*inv) mod p=(a/b) mod p
那麼對於C(n+m)=(n+m)!/(n!*m!)就可以化為(n+m)!(n!*m!)^(p-2) mod p。程式碼與上一種方法類似,在此省略。