组合数的研究

介绍

组合数学算是数学中比较难(虽然被很多人看不起)的一个分类了。
我们可以用组合数学解决很多方案数有关的问题。

定义

组合数学里面有两个大的块。
一个是排列,一个是组合。
所谓排列就是在一个集合中取出的有序子集。
所谓组合就是在一个集合中取出的无序子集。
定义可能看不懂,但举个例子就很简单了。
比如有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;
}

拓展卢卡斯定理

留坑待填
未完待续...

posted @ 2018-10-31 16:05  _onglu  阅读(488)  评论(0编辑  收藏  举报