前言:
在很多问题中都需要计算组合数,在小规模计算中我们可以直接使用组合数公式稍加算法优化进行计算,但在大规模取模计算时往往需要更加快速的算法,接下来主要介绍杨辉三角形法、逆元法(拓欧和费马小定理俩种方法)以及在逆元的基础上进行的阶乘逆元递推优化法
如图是杨辉三角形(帕斯卡三角),是二项式系数在三角形中的几何排列,可以得出一个组合数的规律,具体到公式便是:
C(n, m) = C(n-1, m) + C(n-1, m-1)
由此可以通过一个C_arr数组做一个dp打表得到一定范围内的组合数。
算法特点:
- 算法复杂性=O(n2)
- 数据范围适合在1000以内,尽量不要大于10000.
- 无特殊要求
算法模板:
//方法一:杨辉三角打表法,适合n,m规模均较小,一般适用于1000以内,尽量不超过10000,O(n^2)
//原理: C(n, m) = C(n-1, m) + C(n-1, m-1)
//首先预处理打表数组,每个元素都对应一个组合数C(n,m)
ll C_arr[MAXN + 10][MAXN + 10];
//数据规模n,取模p
void C_init(int n, int p) {
for (int i = 0; i <= n; i++) {
C_arr[i][0] = C_arr[i][i] = 1;
for (int j = 1; j < i; j++)
C_arr[i][j] = (C_arr[i - 1][j - 1] + C_arr[i - 1][j]) % p;
}
}
ll C(int n, int m) {
return C_arr[n][m];
}
逆元是什么:
简单来说,逆元是数论模意义中的倒数,
如a是b模p的逆元,即 a * b = 1(mod p),则k / a = k * b(mod p)。
逆元在求组合数中的作用:
由于组合数公式 C(n,m) = n! / m!(n-m)! 中 分母过大不方便操作,便可利用逆元化除为乘。
转化为:
C(n,m) mod p= (n!/m!(n-m)!) mod p = n!*inv(m!)*inv((n-m)!) mod p
那么怎么求逆元呢,接下来主要介绍拓展欧几里得法和费马小定理法
拓展欧几里得算法:
既可以求出最大公约数,还可以顺带求解出使得: ax + by = gcd(a,b) 的特解 x 和 y
如何求出逆元:
当a,b互质时,gcd(a,b)=1,两边同时mod b后得a*x=1(mod b),即x是a模b的逆元
算法模板:
//欧几里得算法:(这里用不到,仅仅是列出来做个对比)
int gcd(int a, int b)
{
if (a < b) swap(a, b);
return b == 0 ? a : gcd(b, a % b);
}
//拓展欧几里得算法:
ll exgcd(ll a, ll b, ll& x, ll& y)//返回ab最大公因数,x为a%b逆元
{
if (!b)
{
x = 1; y = 0;
return a; //到达递归边界,返回上一层
}
ll r = exgcd(b, a % b, x, y);
ll temp = y; //把x y回归上一层
y = x - (a / b) * y;
x = temp;
return r; //得到a b的最大公因数
}
//求a%b的逆元
ll calInv1(ll a, ll b) {
ll x, y;
ll g = exgcd(a, b, x, y);
if (g != 1) return -1;//a、b互质要求不满足
return (x % b + b) % b;
}
费马小定理:
假如p是质数(这里强调p为质数,否则无效),且gcd(a,p)=1,
那么:
a^(p-1)≡1(mod p)。
如何求出逆元:
变形:
a(p-2)≡a-1(mod p)
即a(p-2)是a%p的逆元。
算法模板:
首先需要用到快速幂算法:
//利用快速幂可以用来快速求a^(p-2)
ll qpow(ll base, ll power, ll mod)
{
ll ans = 1;
base %= mod;
while (power > 0) {
if (power & 1) {//指数为奇数,先乘
ans = ans * base % mod;
}
power >>= 1;//指数取半
base = (base * base) % mod;//基数平方
}
return ans;
}
然后利用快速幂即可得到逆元
//求a%p的逆元
ll calInv2(ll a, ll p)
{
return qpow(a, p - 2, p);
}
在前法的基础上递推计算阶乘的逆元(需要连续大量计算阶乘的逆元时可能需要这么写)
递推性质:
设finv[]数组,finv[x] 表示 x! 的逆元 inv(x!)
通过逆元的性质可以得到反向递推式:
finv[i] = finv[i + 1] * (i + 1) % p
然后我们就知道了所有阶乘的逆元,这样再套到组合数公式的逆元变形中:
C(n,m) mod p= (n!/m!(n-m)!) mod p = n!*inv(m!)*inv((n-m)!) mod p
并在此之前我们对阶乘设立一个数组并且初始化(阶乘数组fac[],fac[x]表示x!)
fac[i] = fac[i - 1] * i % p;
最后的组合数公式为:
C(n,m) % p = fac[n] * finv[m] % p * finv[n-m] % p;
算法模板:
//阶乘逆元递推初始化
void init(ll p)
{
//阶乘数组初始化
fac[0] = 1;
for (int i = 1; i <= MAXN; i++)
fac[i] = fac[i - 1] * i % p;
finv[0] = 1;
//阶乘的逆元数组初始化
finv[MAXN] = calInv2(fac[MAXN], p);
for (int i = MAXN - 1; i > 0; i--)
finv[i] = finv[i + 1] * (i + 1) % p;//阶乘逆元反向递推式
}
//组合数C(n,m)取模p
ll C(ll n, ll m, ll p)
{
if (n < m)
return 0;
return fac[n] * finv[m] % p * finv[n - m] % p;
}
把前面的内容综合起来,就可以拿去做一个完整的组合数取模算法
(1)不使用递推式的算法模板
#include
#include
using namespace std;
typedef long long ll;
const ll MOD = 1e9 + 7;//模取1e9+7
const int MAXN = 1000;//组合数n和m的范围暂且取1000
ll fac[MAXN + 10];//阶乘数组
//拓展欧几里得算法
//返回ab最大公因数,x为a%b逆元
ll exgcd(ll a, ll b, ll& x, ll& y)
{
if (!b)
{
x = 1; y = 0;
return a; //到达递归边界开始向上一层返回
}
ll r = exgcd(b, a % b, x, y);
ll temp = y; //把x y变成上一层的
y = x - (a / b) * y;
x = temp;
return r; //得到a b的最大公因数
}
//求a%b的逆元
ll getInv(ll a, ll b) {
ll x, y;
ll g = exgcd(a, b, x, y);
if (g != 1) return -1;//a、b互质要求不满足
return (x % b + b) % b;
}
//阶乘数组初始化
void fac_init(ll p)
{
int i;
fac[0] = 1;
for (i = 1; i <= MAXN; i++)
{
fac[i] = fac[i - 1] * i % p;
}
}
//组合数函数C(n,m)取模p
ll C(ll n, ll m, ll p)
{
if (n < m)
return 0;
return fac[n] * getInv(fac[m], p) % p * getInv(fac[n - m], p) % p;
}
#include
#include
using namespace std;
typedef long long ll;
const ll MOD = 1e9 + 7;//模取1e9+7
const int MAXN = 1000;//组合数n和m的范围暂且取1000
ll fac[MAXN + 10];//阶乘数组
//快速幂base^power % mod
ll qpow(ll base, ll power, ll mod)
{
ll ans = 1;
base %= mod;
while (power > 0) {
if (power & 1) {//指数为奇数,先乘
ans = ans * base % mod;
}
power >>= 1;//指数取半
base = (base * base) % mod;//基数平方
}
return ans;
}
//求a%p的逆元
ll getInv(ll a, ll p)
{
return qpow(a, p - 2, p);
}
//阶乘数组初始化
void fac_init(ll p)
{
int i;
fac[0] = 1;
for (i = 1; i <= MAXN; i++)
{
fac[i] = fac[i - 1] * i % p;
}
}
//组合数函数C(n,m)取模p
ll C(ll n, ll m, ll p)
{
if (n < m)
return 0;
return fac[n] * getInv(fac[m], p) % p * getInv(fac[n - m], p) % p;
}
(2)使用递推式的算法模板
这里逆元用费马小定理配合快速幂求:
#include
#include
using namespace std;
typedef long long ll;
const ll MOD = 1e9 + 7;//模取1e9+7
const int MAXN = 1000;//组合数n和m的范围暂且取1000
ll fac[MAXN + 10];//阶乘数组
ll finv[MAXN + 10];//阶乘逆元数组
//快速幂base^power % mod
ll qpow(ll base, ll power, ll mod)
{
ll ans = 1;
base %= mod;
while (power > 0) {
if (power & 1) {//指数为奇数,先乘
ans = ans * base % mod;
}
power >>= 1;//指数取半
base = (base * base) % mod;//基数平方
}
return ans;
}
//求a%p的逆元
ll getInv(ll a, ll p)
{
return qpow(a, p - 2, p);
}
//阶乘数组和阶乘的逆元数组初始化
void init(ll p)
{
//阶乘数组初始化
fac[0] = 1;
for (int i = 1; i <= MAXN; i++)
fac[i] = fac[i - 1] * i % p;
finv[0] = 1;
//先求出最大值的逆元作为递推基
finv[MAXN] = getInv(fac[MAXN], p);
//阶乘逆元反向递推式
for (int i = MAXN - 1; i > 0; i--)
finv[i] = finv[i + 1] * (i + 1) % p;
}
//组合数函数C(n,m)取模p
ll C(ll n, ll m, ll p)
{
if (n < m)
return 0;
return fac[n] * finv[m] % p * finv[n-m] % p;
}
当然实际使用时,不要忘记把初始化函数在主函数里调用。
还有一个Lucas法以后用上了再更…