幾種組合數的常見處理方式

前言:
組合數,大家初中就學過,從n個不同元素裏選出m個元素的所有組合的個數,叫做n個不同元素中取出m個元素的組合數。用符號C(n,m) 表示。在數論裏,組合數算是非常基礎的,也經常使用的一種算法,這篇博客主要是討論在算法題目中,經常使用的處理組合數的方式。(如有錯誤,歡迎大家留言指正)

方式一:遞推公式
我們初中就學過,組合數有許多性質,也肯定知道組合數有一個遞推公式。
在這裏插入圖片描述簡單解釋一下,我們從 n 個物體裏選出 m 個物體,那麼對於 n 個物體中的其中一個物體,無疑就只有兩種情況,選擇了這個物體 or 沒有選擇這個物體,那麼選擇了這個物體就有 C(n-1,m-1)種方式(已經確定一個,再選 m-1 個),沒有選擇這個物體就有 C(n-1,m)種方式(沒有確定的,要選 m 個),因此我們可以得到 C(n,m)=C(n-1,m-1)+C(n-1,m)。
在一般題目中,我們可以通過這個遞推公式來預處理組合數,這樣在之後使用組合數時就可以實現O(1)調用了。
代碼段

    for(int i=0;i<=n;i++)//先處理m=0的情況
    C[i][0]=1;
    for(int i=1;i<=n;i++)
    for(int j=1;j<=i;j++)
    C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;//遞推

注意事項
因爲遞推公式要開二維,如果 n 很大,數組會爆,建議如果 n>=1e4 的話要謹慎使用,實測 int 數組 n>=3e4 會爆,long long 數組 n>=2e4 會爆,即使數組沒爆,也有可能爆內存,所以 n>=1e4 要謹慎使用。

方式二:利用公式預處理
前面利用遞推公式的預處理,因爲是二維的,很容易爆內存,所以,我們要想其他辦法,來預處理。我們初中就學過。
在這裏插入圖片描述
一般來說,我們只需要預處理出 1~n 的階乘(通過遞推),就可以通過上面的式子計算出 C(n,m) 了,但是在很多情況下,求組合數,往往伴隨着取模操作,所以不能簡單的通過上面的式子計算了。
因此這裏就涉及了分數取模的問題,這樣的話,在模數爲素數時,我們可以根據費馬小定理來求。
這裏簡要說明一下費馬小定理(不涉及證明)。

費馬小定理
若存在整數 a , p 且gcd(a,p)=1,即二者互爲質數,則有a^(p-1)≡ 1(mod p)。

我們可以根據費馬小定理推出,a(p-2) ≡ a-1 (mod p)。因此我們可以將(mod爲素數) n!-1%mod 轉化成 n!mod-2%mod 通過費馬小定理,之後再用快速冪,就可以轉化成整數取模了。

代碼段

快速冪代碼

long long quickpow(long long x,long long k)
{
    long long res=1;   
    while(k)
    {        
 	if(k&1)//k爲奇數
 	res=res*x%mod;        
 	k>>=1;//k/2
 	x=x*x%mod;    
     }   
     return res;
}

預處理爲下方代碼

    //A[]爲階乘,inv[]爲階乘倒數
    A[0]=1;//0!=1
    for(int i=1;i<=n;i++)//n!=(n-1)!*n
    A[i]=A[i-1]*i%mod;
    //費馬小定理
    inv[n]=quickpow(A[n],mod-2);//quickpow快速冪
    for(int i=n-1;i>=0;i--)//1/n!*n=1/(n-1)!
    inv[i]=inv[i+1]*(i+1)%mod;

組合數就可以由下方代碼得到

long long getC(int n,int m)
{  	
    //A[]爲階乘,inv[]爲階乘倒數
    if(n==m||!m)
    return 1;
    else
    return A[n]*inv[m]%mod*inv[n-m]%mod;
}

注意事項
費馬小定理只能運用在兩數互質的情況下,所以一般題目只有模數爲素數時我們才能使用上面的方法。當然,也要注意 n 的大小,1e8以上最好別使用,會爆內存。

方式三:Lucas定理
當 n>=1e15 並且最好 p<=1e5時,可以用Lucas定理來求解這些大組合數。
Lucas定理如下(馮志剛《初等數論》)
在這裏插入圖片描述
我們可以將上面的定理化爲 C(n,m)%p=C(n/p,m/p)*C(n%p,m%p)%p,這樣我們就可以遞歸的求解了。
代碼段
遞歸

long long C(long long n,long long m)//C(n,m)
{
    if(n<m)
    return 0;
    long long x=1,y=1;
    for(long long i=n;i>n-m;i--)//暴力求階乘
    x=x*i%mod;
    for(long long i=m;i>0;i--)
    y=y*i%mod;
    return x*quickpow(y,mod-2)%mod;//quickpow快速冪
}
long long Lucas(long long n,long long m)//C(n,m)%mod
{
    if(m==0)
    return 1;
    return C(n%mod,m%mod)*Lucas(n/mod,m/mod)%mod;
    
}

非遞歸

long long Lucas(long long n,long long m)//C(n,m)%p
{
    //quickpow快速冪,A[]爲階乘
    long long res=1;
    while(n&&m)
    {
        long long nn=n%mod,mm=m%mod;
        if(nn<mm)
        return 0;
        //1/n!%mod=n^(mod-2)%mod;
        res=res*A[nn]*quickpow(A[mm]*A[nn-mm]%mod,mod-2)%mod;
        n/=mod,k/=mod;
    }
    return res;
}

注意事項
Lucas定理只有當 n,m非常大,p比較小的時候才嫩使用,並且如果 p 不爲素數還得通過質因數分解+中國剩餘定理合併等方式求解(這裏不展開了)。

總結
組合數處理方式比較多,每個方式都有自己適應的條件,我們使用的時候要具體問題具體分析,根據條件選擇合適的方式。歡迎大家評論交流。

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