组合数

组合数

其实我也没想到我第一篇博客会讲一个偏数学的内容。
主要是我太弱了,只会这个

何为组合数

记号\(C_n^m\)表示组合数,其意义为在\(n\)个可区分物品中无序地选择\(m\)个物品的方案数。
如三个数分别为\(1,2,3\),希望选出两个数,则有\((1,2)(1,3)(2,3)\)三种方案。
因为是无序选择,因此\((1,2)\)\((2,1)\)被认为是同一种方案。
因而\(C_3^2=2\).

组合数的简单性质

根据意义,易知:
\(C_n^0=1\)
\(C_n^1=n\)
\(C_n^n=1\)
\(C_n^i=C_n^{n-i}\)
\(C_n^m=0(m>n)\)
\(\sum_{i=1}^nC_n^i=2^n\)

组合数的计算方法

根据组合数的定义,我们可以得到计算公式。
\[C_n^m=\frac{n(n-1)\cdots(n-m+1)}{m!}=\frac{n!}{m!(n-m)!}\]
即统计有序选择的方案数\(n(n-1)\cdots(n-m+1)\),然后除掉每个方案被重复计数的\(m!\)
有了这个式子后,我们只要处理出阶乘及其倒数,便可以计算组合数。
由于组合数一般很大,实际上大多时候在模意义下计算。

组合数还存在递推式。
\[C_n^m=C_{n-1}^{m-1}+C_{n-1}^m\]
从组合意义来考虑该递推式——
\(n\)个数中选\(m\)个,则考虑最后一个数是否被选,然后化为从\(n−1\)个数中选\(m\)个或\(m−1\)个。
该方法复杂度为\(O(n^2)\),但是简单自然,且对模数无特殊要求。

模意义下的组合数计算

对质数\(p\)取模的组合数,是常见的组合数求解形式。
\(C_n^m=\frac{n!}{m!(n-m)!}\)
也就是我们只需要处理阶乘即可。
则可以处理模意义下的阶乘,由于还需要除法,需要顺便处理\(\frac{1}{i!}\).
求逆元可用快速幂解决。
一般预处理只求\(\frac{1}{maxn!}\),然后根据\(\frac{1}{i!}=\frac{1}{(i+1)!}(i+1)\)倒推即可。
相比刚才的方法, 这个方法对模数有要求,复杂度为\(O(n\ log\ n)\).
\(n\)\(m\)都更大的情况呢?

组合数的计算方式

若计算\(C_n^mmod\ p\)\(p\)为质数),则组合数满足以下公式:
\[C_n^mmod\ p=C_{\frac{n}{p}}^{\frac{m}{p}}C_{n\ mod\ p}^{m\ mod\ p}\]
这被称为卢卡斯定理
运用该式可以进行递归计算,且只涉及\(p\)以内的组合数。

而如果要计算\(C_n^mmod\ p^k\)\(p\)为质数),
\(C_n^m=\frac{n!}{m!(n-m)!}\),易知:
我们不妨计算每个阶乘除去\(p\)的部分,这样分母也存在逆元。
这可以分治解决,排除\(p\)后,\(n!\)被分为若干循环外加一段,每个循环长度为\(n^k\)
然后将含有\(p\)的部分除掉\(p\)化为子阶乘递归处理。
最后用库默尔定理计算含有多少\(p\),当然也可以在分治过程中直接统计。
此方法适用于\(p^k\)不大的情况。
可以看出该方法能结合中国剩余定理推广至对一般数\(p\)的组合数取模(要求分解出的\(p^k\)都不大)。
(什么,你问我具体的公式?上面的库默尔定理有下划线发现了吗?我要是会公式我还只讲思路干嘛

例题

例1

给你两个数\(n\)\(m\)
统计有多少长度为\(n\)非负整数序列\(A\),使元素和为\(m\)
答案对\(1000000007\)取模。

\(n,m\leq1000000\)

思路:
\(m\)加上\(n\),则转化为统计正整数序列。
该问题可以这样描述:有\(n\)个盒子,将\(m\)个球放入其中使得每个盒子至少一个的方案数。
我们将球排成一行,考虑将\(n-1\)个隔板插入其中,只能在球与球间插入,且至多插入一个。则与上述问题等价。
从组合意义上可以得知结果为\(C_{m-1}^{n-1}\).
该结论也被称作抽屉原理

例2

在一个网格图中,从\((0,0)\)走到\((n,m)\),要求不能超越\(y=x\)这条斜线,每次往右或往上走长度为\(1\),求方案数。
答案对\(1000000007\)取模。

\(n,m\leq1000000\)

思路:
(第一眼看上去是不是很像卡特兰数我刚开始也这么认为,但很抱歉,不保证\(m=n\)
直接计算走到\((n,m)\)的方案数,可以知道一定有\(n+m\)步,其中有\(n\)步向右,\(m\)步向上。
如何考虑\(y=x\)
不能超越\(y=x\),即不能抵达\(y=x+1\)
对于每一条抵达了\(y=x+1\)的路径,找到最后一个交点,将之后的路径翻转,可以得到一条到\((n,m)\)对称点的路径。
而一条到\((n,m)\)对称点的路径,必定与\(y=x+1\)相交,找到最后一个交点可翻转回去。因此从\((0,0)\)\((n,m)\)关于\(y=x+1\)的对称点的路径数等于非法路径数。

例3 CF785D Anton and School - 2

一个长度为\(n\)的括号序列,问有多少非空子序列长度为偶数,且前一半为左括号后一半为右括号(例如“(())”)。
答案对\(1000000007\)取模。

\(n\leq200000\)

思路:
可以枚举最后一个左括号,假如它左边有\(A\)个左括号,右边有\(B\)个右括号。
贡献为\(\sum C_A^{t-1}C_B^t\)
然而我们无法每次枚举计算该式子。
考虑变化式子为\(\sum C_A^{t-1}C_B^{B-t}\)
可考虑该式子组合意义——
\(A+B\)个球排成一行,选择其中\(B-1\)个球的方案数。
该求和式则是在枚举前\(A\)个球中选\(t-1\)个。
即可以推广出结论——
\[\sum C_A^iC_B^{n-1}=C_{A+B}^n\]
该式被称为范德蒙恒等式

代码(贴一下本蒟蒻的代码):

#include<bits/stdc++.h>
#define N 400010
#define MOD 1000000007

using namespace std;

int l,r,len;
long long ans;
long long fac[N],inv[N];
string str;

long long Pow(long long a,long long p) {
    a%=MOD;
    long long ans=1;
    while(p) {
        if(p&1) {
            ans=ans*a%MOD;
        }
        a=a*a%MOD;
        p>>=1;
    }
    return ans;
}

void Init() {
    fac[0]=1;
    inv[0]=1;
    for(int i=1;i<N;i++) {
        fac[i]=fac[i-1]*i%MOD;
        inv[i]=Pow(fac[i],MOD-2);
    }
    return;
}

long long Calc(int m,int n) {
    if(m<n||m<0) {
        return 0;
    }
    long long res=fac[m]*inv[m-n]%MOD*inv[n]%MOD;
    return res;
}

int main()
{
    cin>>str;
    len=str.length();
    Init();
    for(int i=0;i<len;i++) {
        if(str[i]==')') {
            r++;
        }
    }
    for(int i=0;i<len;i++) {
        if(str[i]==')') {
            r--;
        }
        else {
            ans=(ans+Calc(l+r,l+1))%MOD;
            l++;
        }
    }
    cout<<ans;
    return 0;
}

小结

组合数应用十分广泛,在信奥中常作为数论试水题出现,也经常出没在各大省选中。而在高数联中,更是作为入门第一讲(排列组合)。所以学好组合数是非常必要的。
等我有时间再写一个奥数的组合数(乱立Flag)

posted @ 2019-08-24 18:06 华风の洛水天依 阅读(...) 评论(...) 编辑 收藏