組合數詳解

概念:
組合數我們用C(n,m)表示,它代表在n個數中取m個數的方案。(這個概念主要用於將問題抽象到組合數上)。
公式:
組合數的公式也不多,
1、C(n,m)=C(n,n-m)。
2、C(n,m)=C(n-1,m-1)+C(n-1,m)。這個很重要,因爲這個和楊輝三角的遞推公式一樣的,所以我們經常把楊輝三角和組合數和起來看。典題
3、C(0,n)+C(1,n)+C(2,n)+C(3,n)+…C(n,n)=2 ^ n,這個公式被我們成爲二次項定理,這個也經常用。
這上面三個就是我們經常用的(省選大佬出門左拐)。
求法:
對於組合數的求法挺多的:Lucas定理、遞推、逆元。
遞推:
這個其實就是在推楊輝三角,這個主要是用於n和m<=2000並且要用到很多的時候用的。舉個栗子
對於這種問題我們就比較適合遞推。
適用範圍:
n<=2000(n是組合數的下標的值,即C(n,m)中的n)。
板子代碼:

#include<iostream>
#include<cstdio>
using namespace std;
int C[1000][1000],mod=1e9+9;
int main()
{
	int n,m;
	scanf("%d %d",&n,&m);
	for(int i=0;i<=n;i++)//邊界一定要全 
	{
		C[i][1]=i%mod;//對於i個數中取一個數方案是i 
		C[i][i]=1;//在i個數中取i個數只有一種方案 
	}
	for(int i=2;i<=n;i++)//直接開始遞推 
		for(int j=2;j<i;j++)//邊界不要計算 
			C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;//根據第二個公式,也就是楊輝三角的遞推式 
	printf("%d",C[n][m]);//輸出答案 
	return 0;
}

題目代碼:

#include<iostream>
#include<cstdio>
using namespace std;
long long C[2010][2010],sum[2010][2010],mod;
void pre()
{
	for(int i=0;i<=2000;i++)
	{
		C[i][1]=i%mod;
		C[i][i]=1;
	}
	for(int i=2;i<=2000;i++)
		for(int j=2;j<i;j++)
			C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
	for(int i=1;i<=2000;i++)
		for(int j=1;j<=i;j++)//預處理出在i個數中1到j中有多少個組合數是mod的倍數 
			if(C[i][j]==0)//因爲對mod取餘,所以等於0就是mod的倍數 
				sum[i][j]=sum[i][j-1]+1;//前綴和計算 
			else sum[i][j]=sum[i][j-1];//不是的話就直接等於 
	return ;
}
int main()
{
	int n,m,T;
	scanf("%d %d",&T,&mod);//多組測試數據 
	pre();//直接求出2000以內所有組合數 
	while(T--)
	{
		long long ans=0;
		scanf("%d %d",&n,&m);
		for(int i=1;i<=n;i++)
			if(i>m)//這個要注意,因爲如果i<m的話min(i,m)是等於i的 
				ans+=sum[i][m];
			else ans+=sum[i][i];
		printf("%lld\n",ans);//輸出答案 
	}
	return 0;
}

階乘逆元:
這個方法就是根據組合數的定義公式去求,根據C(n,m)=n!/(m!*(n-m)!),所以我們要預處理出所有的階乘以及逆元。首先你要會階乘逆元的預處理。不會階乘逆元的點這
適用範圍:
n<mod(n的意義上同,因爲如果n>mod的話,其中有的階乘就會因爲是mod的倍數而沒有逆元,這樣就錯了)
板子代碼(費馬小定理):

#include<iostream>
#include<cstdio>
using namespace std;
long long inv[1000100],jc[1000100],mod=1e9+9;
long long pow(long long a,long long b)//快速冪 
{
	long long ans=1;
	while(b)
	{
		if(b%2==1)
			ans=(ans*a)%mod;
		a=(a*a)%mod;
		b=b>>1;
	}
	return ans;
}
void pre()
{
	jc[1]=1;//階乘邊界 
	for(int i=2;i<=1000000;i++)
		jc[i]=(jc[i-1]*i)%mod;//求階乘 
	inv[1000000]=pow(jc[1000000],mod-2);//用費馬小定理求 
	for(int i=999999;i>=0;i--)
		inv[i]=(inv[i+1]*(i+1))%mod;//倒推求階乘逆元 
	return ;
}
int main()
{
	pre();
	int T;
	scanf("%d",&T);
	while(T--)
	{
		long long n,m;
		scanf("%lld %lld",&n,&m);
		printf("%lld\n",(((jc[n]*inv[m])%mod)*inv[n-m])%mod);//求答案,記得mod,一直mod,乘完就mod! 
	}
}

板子代碼(拓展歐幾里得):

#include<iostream>
#include<cstdio>
using namespace std;
int mod=1e9+9,inv[1000100],jc[1000100],x,y;
void gcd(int a,int b)//拓展歐幾里得 
{
	if(b==0)
	{
		x=1;
		y=0;
		return ;
	}
	gcd(b,a%b);
	int k=x;
	x=y;
	y=k-a/b*y;
	return ;
}
void pre()
{
	jc[1]=1;
	for(int i=2;i<=1000000;i++)//求階乘 
		jc[i]=(jc[i-1]*i)%mod;
	gcd(jc[1000000],mod);//求最大階乘逆元 
	inv[1000000]=(x+mod)%mod;//賦值 
	for(int i=999999;i>=0;i--)//倒推階乘逆元 
		inv[i]=(inv[i+1]*(i+1))%mod;
	return ;
}
int C(int n,int m)
{
	return (((1LL*jc[n]*inv[m])%mod)*inv[n-m])%mod;//組合數 
}
int main()
{
	int T;
	scanf("%d",&T);
	pre();
	while(T--)
	{
		int n,m;
		scanf("%d %d",&n,&m);//讀入 
		printf("%d\n",C(n,m));//輸出答案 
	}
	return 0;
}

Lucas定理:
這個定理個人覺得還好,主要是針對mod很小,如果用階乘的話就有可能會是mod的倍數,這樣逆元求組合數就gg了。可是對於遞推又被卡的情況下我們就只能用這個了。
適用範圍:
在遞推和階乘逆元都掛的時候,就用這個。
公式:
Lucas(n,m,mod)(n和m是C(n,m)中的,mod就是取模對象)=C(n%mod,m%mod) * Lucas(n/mod,m/mod,mod)。
代碼:
這個代碼有兩種,因爲我們可以看見,在公式中我們是有組合數的,只不過範圍降了下來,所以我們主要是用Lucas定理把原本卡死遞推和階乘逆元給救回來。所以我們纔有兩種代碼。當mod比較小時我們可以採用遞推,而mod比較大時我們可以用階乘逆元。
遞推:

#include<iostream>
#include<cstdio>
using namespace std;
int C[2010][2010],mod;
void pre()//預處理 
{
	for(int i=1;i<=mod;i++)
	{
		C[i][1]=i%mod;
		C[i][i]=1;
	}
	for(int i=2;i<=mod;i++)
		for(int j=2;j<i;j++)
			C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
	return ;
}
int Lucas(int n,int m)
{
	if(m==0)//邊界 
		return 1;
	return (C[n%mod][m%mod]*Lucas(n/mod,m/mod))%mod;//Lucas定理的公式 
}
int main()
{
	int T;
	scanf("%d %d",&T,&mod);
	pre();
	while(T--)
	{
		int n,m;
		scanf("%d %d",&n,&m);
		printf("%d\n",Lucas(n,m));//Lucas定理 
	}
	return 0;
}

階乘逆元:
對於這個雖然有了小優化,可還是有一個條件:mod必須是質數纔可以,要不然題目保證mod以內的階乘不會是mod的倍數。
費馬小定理:

#include<iostream>
#include<cstdio>
using namespace std;
int jc[1000100],inv[1000100],mod;
int pow(long long a,long long b)//快速冪 
{
	long long ans=1;
	while(b)
	{
		if(b%2==1)
			ans=(ans*a)%mod;
		a=(a*a)%mod;
		b=b>>1;
	}
	return (int)ans;
}
void pre()//預處理階乘逆元 
{
	jc[1]=1;
	for(int i=2;i<mod;i++)
		jc[i]=(jc[i-1]*i)%mod;
	inv[mod-1]=pow(jc[mod-1],mod-2);//費馬小定理 
	for(int i=mod-2;i>=0;i--)
		inv[i]=(inv[i+1]*(i+1))%mod;
}
int C(int n,int m)//組合數公式 
{
	return (((jc[n]*inv[m])%mod)*inv[n-m])%mod;
}
int Lucas(int n,int m)
{
	if(m==0)//邊界 
		return 1;
	return (C(n%mod,m%mod)*Lucas(n/mod,m/mod))%mod;//Lucas定理公式 
}
int main()
{
	int T;
	scanf("%d %d",&T,&mod);
	pre();
	while(T--)
	{
		int n,m;
		scanf("%d %d",&n,&m);
		printf("%d\n",Lucas(n,m));//輸出 
	}
	return 0;
}

拓展歐幾里得:

#include<iostream>
#include<cstdio>
using namespace std;
int mod,inv[1000100],jc[1000100],x,y;
void gcd(int a,int b)//拓展歐幾里得 
{
	if(b==0)
	{
		x=1;
		y=0;
		return ;
	}
	gcd(b,a%b);
	int k=x;
	x=y;
	y=k-a/b*y;
	return ;
}
void pre()//預處理 
{
	jc[1]=1;
	for(int i=2;i<mod;i++)
		jc[i]=(jc[i-1]*i)%mod;
	gcd(jc[mod-1],mod);//處理出解 
	inv[mod-1]=(x+mod)%mod;//把解取正 
	for(int i=mod-2;i>=0;i--)
		inv[i]=(inv[i+1]*(i+1))%mod;
	return ;
}
int C(int n,int m)//組合數嘗龜 
{
	return (((1LL*jc[n]*inv[m])%mod)*inv[n-m])%mod;
}
int Lucas(int n,int m)
{
	if(m==0)
		return 1;
	return (1LL*C(n%mod,m%mod)*Lucas(n/mod,m/mod))%mod;
}
int main()
{
	int T;
	scanf("%d %d",&T,&mod);//多組測試數據,mod的值 
	pre();
	while(T--)
	{
		int n,m;
		scanf("%d %d",&n,&m);
		printf("%d\n",Lucas(n,m));//輸出 
	}
	return 0;
}

以上就是組合數提高組的所有知識,希望大家看後可以有理解。
如果有不清楚的歡迎留言詢問。

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