Skip to content

组合数

n个不同元素种取出m(mn)个元素的所有不同组合的个数,叫做从n个不同元素种取出m个元素的组合数,用符号Cnm表示。1

基本公式

Cnm=AnmAmm=n(n1)(n2)(nm+1)m!=n!m!(nm)!,n,mN,并且mnCn0=Cnn=1

性质

Cnm=CnnmCnm=Cn1m+Cn1m1

很好理解, 第一个式子可以看作选取之前没有选取的元素的方案就等于之前的方案,因为它们之间是一一对应的。 第二个式子可以看作把当前选取方案拆分成两部分,一部分含有特定元素,一部分不含特定元素,就能将当前方案数不重不漏地分好。

求和公式

Cn0+Cn1+Cn2++Cnn=2n

证明:

n=1时,C10+C11=2=21。 假设n=k(kN)时成立,即

i=0kCki=2n

成立 当n=k+1时,

Ck+10+Ck+11+Ck+12++Ck+1k+Ck+1k+1=Ck+10+(Ck0+Ck1)+(Ck1+Ck2)++(Ckk1+Ckk)+Ck+1k+1=(Ck0+Ck1+Ck2++Ckk)+(Ck0+Ck1+Ck2++Ckk)=2×2k=2k+1

所以等式对nN都成立。


求解方法

Cabmodp

递推式求解 O(n2)

1n10000,1ba2000

直接应用 Cnm=Cn1m+Cn1m1

cpp
//Author: CodeBoy

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 2010,mod = 1e9+7;
int C[N][N];
void init(){
    for(int i=0;i<N;i++){
        for(int j=0;j<=i;j++){
            if(j==0) C[i][j] = 1;
            else{
                C[i][j] = (C[i-1][j-1]+C[i-1][j]) % mod;
            }
        }
    }
}
int main(){
    init();
    int n;
    cin>>n;
    for(int i=0;i<n;i++){
        int a,b;
        cin>>a>>b;
        cout<<C[a][b]<<endl;
    }
    return 0;
}

预处理阶乘及其逆元 O(alog(p))

1n10000,1ba105

原理:

Cab=a!b!(ab)!=a!b!1(ab)!1

p 为质数,所以逆元可用费马小定理求

cpp
//Author: CodeBoy

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

typedef long long ll;
const int N = 100010, mod =1e9+7;
int fact[N],invfact[N];
ll qpow(ll a,int b,int p){ //因为a会被算平方所以要用ll否则可能会溢出
    ll res=1;
    while(b){
        if(b&1) res = res * a % p;
        b>>=1;
        a = a*a % p;
    }
    return res;
}

int main(){
    fact[0] = invfact[0] = 1;
    for(int i=1;i<N;i++){ //递推阶乘及逆元
        fact[i] = (ll)fact[i-1] * i % mod;
        invfact[i] = (ll)invfact[i-1] * qpow(i,mod - 2 ,mod) % mod;
    }
    int n;
    scanf("%d",&n);
    while(n--){
        int a,b;
        scanf("%d%d",&a,&b);
        printf("%d\n",(ll)fact[a]%mod * invfact[b]%mod * invfact[a-b]%mod); //查表输出答案
    }
    return 0;
}

Lucas定理求模意义下组合数 O(logpN(p+logp))

1n20,1ba1018,1p105

Lucas 定理是同余理论中的一个很重要的定理,用于组合数取模。常常使用在问题的结果需要对某个数据取模,n,m 很大,达到 1015 以上,但是 p109 以内。一般来说最好的效果实在 105 以内。

Cmn=CmmodpnmodpCmpnp(modp)
cpp
//Author: CodeBoy

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
int qpow(int a, int k, int p)
{
    int res = 1;
    while (k)
    {
        if (k & 1) res = (LL)res * a % p;
        a = (LL)a * a % p;
        k >>= 1;
    }
    return res;
}
LL C(LL a, LL b, int p)
{
    if(b > a) return 0;
    if(b > a - b) b = a - b;
    LL x = 1, y = 1;
    for(int i = 0; i < b; i++)
    {
        x = x * (a - i) % p;
        y = y * (i + 1) % p;
    }
    return x * qpow(y, p - 2, p) % p;
}
int lucas(LL a, LL b, int p)
{
    if (a < p && b < p) return C(a, b, p);
    return (LL)C(a % p, b % p, p) * lucas(a / p, b / p, p) % p; //递归求解, 多次使用 Lucas 定理加快速度
}
int main(){
    int n;
    cin >> n;

    while (n -- )
    {
        LL a, b;
        int p;
        cin >> a >> b >> p;
        cout << lucas(a+b, b, p) << endl;
    }
    return 0;
}

Lucas 定理的证明

2 考虑 (pn)modp 的取值,注意到 (pn)=p!n!(pn)!,分子的质因子分解中 p 次项恰为 1,因此只有当 n=0n=p 的时候 n!(pn)! 的质因子分解中含有 p,因此 (pn)modp=[n=0n=p]。进而我们可以得出

(3)(a+b)p=n=0p(pn)anbpn(4)n=0p[n=0n=p]anbpn(5)ap+bp(modp)

注意过程中没有用到费马小定理,因此这一推导不仅适用于整数,亦适用于多项式。因此我们可以考虑二项式 fp(x)=(axn+bxm)pmodp 的结果

(6)(axn+bxm)papxpn+bpxpm(7)axpn+bxpm(8)f(xp)

考虑二项式 (1+x)nmodp,那么 (nm) 就是求其在 xm 次项的取值。使用上述引理,我们可以得到

(9)(1+x)n(1+x)pn/p(1+x)nmodp(10)(1+xp)n/p(1+x)nmodp

注意前者只有在 p 的倍数位置才有取值,而后者最高次项为 nmodpp1,因此这两部分的卷积在任何一个位置只有最多一种方式贡献取值,即在前者部分取 p 的倍数次项,后者部分取剩余项,即 (nm)modp=(n/pm/p)(nmodpmmodp)modp

参考