求组合数算法总结

组合数概念

C(a,b) = a ! b ! ( a − b ) ! \frac{a!}{b!(a-b)!} b!(ab)!a! = a ∗ ( a − 1 ) ∗ ( a − 2 ) ∗ . . . . ∗ ( a − b + 1 ) b ! \frac{a * (a-1)*(a-2)*....*(a-b+1)}{b!} b!a(a1)(a2)....(ab+1)
C(a,0) = 1
意思是从 a 个苹果里选 b 个苹果,共有 C(a,b) 种选法。

性质:C(a,b) = C(a-1,b) + C(a-1,b-1)

怎样理解这个性质呐?
先从 a 个苹果里挑出 1 个苹果 ,那么选法就有两种,①包含这 1 个苹果,那么只用从剩下的 a - 1 个中选 b - 1 个,即 C(a-1,b-1) ②不包含这 1 个苹果,那么需要从剩下的 a - 1 个中选 b 个,即 C(a-1,b)。


求组合数Ⅰ

题目: n 组询问,每组给出两个数 a、b,求 C(a,b) mod (109 + 7)

1 ≤ n ≤ 100000
1 ≤ b ≤ a ≤ 2000

分析:
每算一个组合数,最坏需要循环 2000 次,一共有 100000 次询问,总共时间复杂度就是 2000 * 100000 = 2 * 108,这样算是会超时的。

但是,C(a,b)中所有不同的 a 和 b 只有 20002 = 4 * 106 对儿, 我们可以先预处理出所有的C(a,b),每一次查询时间是 O(1),总共时间就是 4 * 106 ,时间复杂度O(n2)。

怎样预处理呢? 根据这个递推式即可,C(a,b) = C(a-1,b) + C(a-1,b-1)

#include<iostream>
#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) c[i][j] = 1;  // C(a,0) = 1 
	    else c[i][j] = (c[i-1][j] + c[i-1][j-1]) % mod;
}

int main()
{
   init();	
   int n;
   cin >> n;
   while(n--)
   {
   	 int a,b;
   	 cin >> a >> b;
   	 cout << c[a][b] <<endl;
   }
} 

求组合数Ⅱ

题目: n 组询问,每组给出两个数 a、b,求 C(a,b) mod (109 + 7)

1 ≤ n ≤ 100000
1 ≤ b ≤ a ≤ 105

分析:
这道题a、b的数据范围变了,显然,不能再用第一种方法了。
根据定义做,C(a,b) = a ! b ! ( a − b ) ! \frac{a!}{b!(a-b)!} b!(ab)!a!

因为 a ! b ! \frac{a!}{b!} b!a! % p ≠ a ! % p b ! % p \frac{a! \% p}{b !\% p} b!%pa!%p,所以我们可以通过快速幂来求 b! 的逆元。(快速幂及逆元讲解)

我们先预处理出 i 的阶乘 fact[i] 和 i 的阶乘的逆元 infact[i] 。那么C(a,b) = fact[a] * infact[b] * infact[a-b]
时间复杂度是(nlogn)。

#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 100010,mod = 1e9 + 7;

int fact[N],infact[N]; 
// fact[i]是 i 的阶乘,infact[i]是 i 阶乘 % mod的逆元 

int qmi(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;
}
 
int main()
{
	fact[0] = infact[0] = 1;  
	for(int i=1; i<N; i++) //预处理出阶乘、阶乘的逆元
	{
	   fact[i] = (LL)fact[i-1] * i % mod; 
	   infact[i] = (LL)infact[i-1] * qmi(i,mod-2,mod) % mod;	
	} 
	int n;
	cin >> n;
	while(n--)
	{
		int a,b;
		cin >> a >> b;
		cout<<(LL)fact[a] * infact[b] % mod * infact[a-b] % mod<<endl; //这里要及时取mod,2个10^9相乘不会溢出long long,但三个相乘就会溢出了! 
	}
}

求组合数Ⅲ

题目: n 组询问,每组询问给a、b、p,其中 p 是质数,求每组的 C(a,b) mod p 的值。

1 ≤ n ≤ 20
1 ≤ b ≤ a ≤ 1018
1 ≤ p ≤ 105

分析:
这个数据范围如果还用 nlogn 的算法是超时的,但我们可以用 lucas定理。这个定理用来求大组合数 C(a,b) % p 的值,p 需是质数
Lucas定理:C(a,b) = C(a%p,b%p) * C(a/p,b/p) % p

下面代码求C时用的是C(a,b) = a ∗ ( a − 1 ) ∗ ( a − 2 ) ∗ . . . . ∗ ( a − b + 1 ) b ! \frac{a * (a-1)*(a-2)*....*(a-b+1)}{b!} b!a(a1)(a2)....(ab+1) ,你也可以用 a ! b ! ( a − b ) ! \frac{a!}{b!(a-b)!} b!(ab)!a!

#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
int p;
int qmi(int a,int k) //快速幂求逆元
{
	int res = 1;
	while(k)
	{
		if(k&1) res = (LL)res * a % p;
		a = (LL)a * a % p;
		k >>= 1;
	}
	return res;
}

int C(int a,int b)
{
	int res = 1;
	for(int i=1, j=a; i<=b; i++,j--)
	{
		res = (LL)res * j % p; // 分子 
		res = (LL)res * qmi(i,p-2) % p; //分母 
	}
	return res;
}

int lucas(LL a,LL b) // lucas定理 
{
	if(a<p && b<p) return C(a,b);
	return (LL)C(a % p,b % p) * lucas(a / p,b / p) % p;  
}

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

求组合数Ⅳ

题目: 输入a、b,求C(a,b),注意结果可能很大,需要使用高精度计算。

1 ≤ b ≤ a ≤ 5000

分析:
1、先筛出 1~N 内所有的质数 (素数筛)

2、将C(a,b) = a ! b ! ( a − b ) ! \frac{a!}{b!(a-b)!} b!(ab)!a!

进行分解质因数,变为p1a1 * p2a2 * …* pkak,每个 p 出现的次数就是分子出现的次数减去分母出现的次数,即代码里的 sum[i] = get(a,p) - get(b,p) - get(a-b,p)。

n! 中 p 的次数 = n/p + n/p2 + n/p3 +…,直到p的某个次方大于等于 n。 对应下面的代码,n/p 是 n! 中 p 的倍数的个数, n/p2 是 n! 中 p2 的倍数的个数…依次类推,累加在一起就是 p 的次数。

int get(int n,int p) // 求 n! 里的某个质数 p 的个数 
{
	int res = 0;
	while(n)
	{
		res += n/p;
		n /= p;
	}
	return res;
}

3、再使用高精度乘法计算出p1a1 * p2a2 * …* pkak (高精度)

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 5010;
int primes[N],cnt;
int sum[N];
bool st[N];

void get_primes(int n) //欧拉筛 
{
	for(int i=2; i<=n; i++)
	{
		if(!st[i]) primes[cnt++] = i;
		for(int j=0; primes[j] <= n/i; j++)
		{
			st[primes[j] * i] = true;
			if(i % primes[j] == 0) break;
		}
	}
}

int get(int n,int p) //  求 n! 里的某个质数 p 的个数 
{
	int res = 0;
	while(n)
	{
		res += n/p;
		n /= p;
	}
	return res;
}

vector<int> mul(vector<int> a,int b) //高精度乘法 
{
	vector<int> c;
	int t=0; //进位 
	for(int i=0; i<a.size() || t; i++)
	{
	    if(i<a.size())	t +=a[i]*b;
		c.push_back(t%10);
		t /= 10;
	}
	return c;
}

int main()
{
	int a,b;
	cin >> a >> b;
	get_primes(a);
	
	for(int i=0; i<cnt; i++) //枚举每个质数p的次数
	{
		int p = primes[i];
		sum[i] = get(a,p) - get(b,p) - get(a-b,p);
	} 
	
	vector<int> res;
	res.push_back(1); //res初始等于1 
	for(int i=0; i<cnt; i++) //枚举所有的质数
	   for(int j=0; j<sum[i]; j++) //枚举这个质数的次数
	     res = mul(res,primes[i]);
		 
    for(int i=res.size()-1; i>=0; i--) cout<<res[i];		  
}
  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值