介绍
组合数学算是数学中比较难(虽然被很多人看不起)的一个分类了。
我们可以用组合数学解决很多方案数有关的问题。
定义
组合数学里面有两个大的块。
一个是排列,一个是组合。
所谓排列就是在一个集合中取出的有序子集。
所谓组合就是在一个集合中取出的无序子集。
定义可能看不懂,但举个例子就很简单了。
比如有n个人,我们从中取出m个人。
把他们排成队,就是排列了。如果不关心顺序,只关心取出哪些人就是组合了。
下面介绍排列组合的几个基本定理和公式。
国际上一般记组合数为$\binom{n}{m} $
但是这里我习惯用国内的记法\(C_{n}^{m}\)
公式
基本计算公式
1.排列数计算公式
\[A_{n}^{m}=\frac{m!}{n!}\]
2.组合数计算公式
\[C_{n}^{m} = \frac{n!}{m!(n-m)!}\]
\[C_{n}^{m} = \frac{\prod_{i=n}^{n-m+1}i}{m!}\]
注意这里!表示阶乘,第二个公式是手动化简公式。
3.组合数递推公式
\[C_{n}^{m} =C_{n-1}^{m-1} +C_{n-1}^{m} \]
证明很简单,在n个元素里取m个元素的方案数=取第m个元素的方案数+不取第m个元素的方案数。
4.卢卡斯(Lucas)定理
\[C_{n}^{m} \%p =C_{n\%p }^{m\%p } \times C_{n/p}^{m/p} \]
注意,这里p必须是质数。
公式不长记住就行,懒得证明。
5.组合数的和
\[\sum{C_n^i}(0\le i\le n)=2^n\]
算法
暴力算法
暴力组合数
int C(int n,int k){
int ans=1;
for(int i=n;i>n-k;i--)ans*=i;
for(int i=1;i<=k;i++)ans/=i;
return ans;
}
杨辉三角求组合数
void init(){
sj[0][0]=1;
for(int i=1;i<=2004;i++){
sj[i][0]=1;
for(int j=1;j<=i;j++)
sj[i][j]=(sj[i-1][j]+sj[i-1][j-1])%mod;
}
}
int C(int m,int k){
return sj[m][k];
}
线性组合数
(前置技能:线性求逆元)
我们发现\[C_{n}^{m} = \frac{n!}{m!(n-m)!}\]
记\[A[n]=n!%p\]
\[B[n]=\prod_{i=1}^{n}{inv[n]}%p\]
则
\[C_{n}^{m} = A[n] \times B[m] \times B[n-m]\]
我们只要线性预处理出阶乘数组和逆元前缀积数组就可以了。
先求出1到n的所有数在模p意义下的逆元。
\[inv[i]=(p-\frac{p}{i})\times inv[p\%i]\%p\]
\[B[i+1]=B[i]\times inv[i+1]%p\]
\[A[i+1]=A[i]\times (i+1)%p\]
这样子我们就可以直接计算了
int fac[100009],inv[100009],n=100005;
void init(){
inv[0]=inv[1]=fac[0]=1;
for(int i=2;i<=n;i++)
inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
for(int i=2;i<=n;i++)inv[i]=1ll*inv[i-1]*inv[i]%mod;
for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod;
}
int C(int n,int m){
return 1ll*fac[n]*inv[m]%mod*inv[n-m]%mod;
}
int main()
{
init();
cout<<C(5,2)<<endl;
return 0;
}
卢卡斯定理
卢卡斯定理用于解决n,m特别大而模数p不是很大的情况。
利用卢卡斯定理,在\(C_{n}^{m}\)特别大的时候不断分解,当n,m小于p时进行组合运算。
int Lucas(int n,int m,int p){
if(m>n)return 0;
if(n<p)return C(n,m);
else return Lucas(n/p,m/p,p)*Lucas(n%p,m%p,p)%p;
}
拓展卢卡斯定理
留坑待填
未完待续...