组合数的计算公式:c(a,b) = ( a*(a-1)* … (a-b+1) ) / (b(b-1)* … * 1) = a! / ( b! * (a-b)! )
常用的组合数的递推式:c(a,b)=c(a-1,b)+c(a-1,b-1)
给出一个实际的应用场景来解释这个递推式。从a个苹果中选出b个,所有情况都可以分为两大类(选中一个苹果标记为红色,作为 划分的标准)。
第一类:选的当中包含红色的【说明还要从剩余的(a-1)个苹果里选出(b-1)个】;
第二类:选的当中不包含红色的【说明还要从剩余的(a-1)个苹果里选出b个】;
例题
思路
n为1e5,a、b为 2x1e3,2x1e8直接暴力会TLE
2000^2000=4*1e6 所以先预处理出所有情况的方案数,每次只需要做一次查询就行了
时间复杂度:O(N^2)
// AC代码
#pragma GCC optimize(3 , "Ofast" , "inline")
#include <bits/stdc++.h>
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;
else c[i][j] = (c[i-1][j] + c[i-1][j-1]) %mod;
}
int main()
{
init();
int n;
scanf("%d",&n);
while(n--)
{
int a,b;
scanf("%d %d",&a,&b);
printf("%d\n",c[a][b]);
}
return 0 ;
}
难度提升1:查询的次数不变,每次查询的数据变大
时间复杂度:O(N*logN)
当 a,b扩大到1e5时,需要运用 逆元 和 快速幂 并 预处理所有的阶乘
逆元阶乘可以直接相乘,也就是说a的逆元乘上b的逆元就是a*b的逆元【证明:如果x是a的逆元,y是b的逆元,那么ax=1,by=1,所以abxy ≡1,所以xy是ab的逆元】
// AC代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N=100010, mod=1e9+7;
int fact[N], infact[N];
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;
scanf("%lld",&n);
while(n--)
{
int a,b;
scanf("%lld %lld",&a,&b);
//三个阶乘相乘会溢出ll,所以要及时模
printf("%d\n",(ll)fact[a] * infact[b] %mod * infact[a-b] %mod);
}
return 0;
}
难度提升2:查询的次数很少,但是每次查询的数据暴大
需要用到 卢卡斯定理(Lucas):c(a,b) ≡ c(a mod p , b mod p) * c(a/p , b/p) [≡ 表示在mod p 的情况下同余 ]
Lucas的证明不需要掌握,会用上面的定理即可!!!【注意:当a和b都小于p时,可以直接从定义出发去做】
下面搞一蛤 Lucas 的简单(LJ)证明:
先将a和b分别变成一个类似p进制的东西
接着用生成函数的方法可以证明
因为p是质数,不包含任何小于p的质因子,所以中间这些项模上p的余数都为0 (因为分母里面有p,分子里面一定没有p)
// AC代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
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--)
{
// 乘j,除以i 等于 乘j,乘i的逆元
res = (ll)res*j%p;
res = (ll)res*qmi(i,p-2)%p;
}
return res;
}
int lucas(ll a,ll b)
{
if(a<p&&b<p) return C(a,b); //当 a和 b都小于 p 时,从定义出发来算的那种情况
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;
}
return 0;
}
难度提升3:组合数不能取模,结果会很大,需要用大数运算(高精度乘和高精度除)
直接根据公式用高精度就的话,时间效率会比较低,而且也比较难写。
一般的处理方式:
第一步:筛素数, 把1~5000内的素数筛出来。
第二步:求每个质数的一个次数, 利用a!=...这个公式来(能够保证不重不漏)
【 不重不漏的解释:假设一个数是p^k的倍数,那这个数里有k个p,那么一定会恰好被+k次
(因为算p的倍数的时候+了一次,算p^2的倍数的时候+了一次...算p^k的倍数的时候+了一次) 】
第三步:用高精度乘法把所有的质因子乘到一块儿去。
// AC代码
#include <bits/stdc++.h>
#include <vector>
#define ll long long
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; //表示进位,最开始的时候初始化为0
for(int i=0; i<a.size(); i++)
{
t += a[i]*b; //a[i]表示当前这一位
c.push_back(t%10);
t /= 10;
}
while(t)
{
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++) //枚举一下每一个质数
{
int p = primes[i]; //当前这个质数
sum[i] = get(a,p) - get(b,p) - get(a-b,p); //求一下当前这个数里包含的p的次数是多少
}
//用高精度乘把所有的质因子乘起来
vector<int> res;
res.push_back(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--)
printf("%d",res[i]);
puts("");
return 0;
}