数论篇7——组合数 & 卢卡斯定理(Lucas)
组合数
组合数就是高中排列组合的知识,求解组合数C(n,m),即从n个相同物品中取出m个的方案数。
求解方式
求解通式:$C^{m}_{n}=\dfrac {n!}{m!\left( n-m\right) !}$
性质1:$C^{m}_{n}=C_{n}^{n-m}$
性质2:$C^{m}_{n}=C^{m-1}_{n-1}-i+C^{m}_{n-1}$
打表递推
根据性质2:$C^{m}_{n}=C^{m-1}_{n-1}+C^{m}_{n-1}$
组合数算出来特别大,往往都会要求取余,这里取$P=1e9+7$。时间复杂度$O(n^2)$
const int P = 1e9 + 7; #define N 1000 int comb[N][N]; int main() { for (int i = 0; i < N; i++) { comb[i][0] = comb[i][i] = 1; for (int j = 1; j < i; j++) { comb[i][j] = comb[i - 1][j] + comb[i - 1][j - 1]; comb[i][j] %= P; //cout << comb[i][j] << endl; } } }
逆元法
因为大部分题都有求余,可利用逆元的原理(没求余的题目,自己找一个比较大的素数作为P,也可以用逆元做)
线性递推求逆元
当$p$为质数时有$a^{-1}=(p-[p/a])\cdot (p\%a)^{-1}\%p$
求阶乘的逆元
根据通式:$C^{m}_{n}=\dfrac {n!}{m!\left( n-m\right) !}$,有$C^{m}_{n}=n!\cdot inv[m!] \cdot inv[(n-m)!]$
设 $finv(i)=inv(i\ !)$
则根据:$finv(i-1)=\frac{1}{\ (i-1)\ !}=\frac{1}{i\ !}\times i =finv(i)\times i$
有:$finv(i) = finv(i-1)\times inv(i)$
初始化时间复杂度$O(n)$,求$C^{m}_{n}$为$O(1)$
const int N = 200000; const int P = (int)1e9 + 7; int fact[N+5], Finv[N+5], inv[N+5];//fact是阶乘,Finv是阶乘的逆元 void init() { inv[1] = 1; //线性递推求逆元 for (int i = 2; i <= N; i++) { inv[i] = (P - P / i) * 1ll * inv[P % i] % P; } fact[0] = Finv[0] = 1; for (int i = 1; i < N; i++) { fact[i] = fact[i - 1] * 1ll * i % P;//求阶乘 Finv[i] = Finv[i - 1] * 1ll * inv[i] % P;//求阶乘的逆元 } } int C(int n, int m) {//comb(n, m)就是C(n, m) if (m < 0 || m > n) return 0; return fact[n] * 1ll * Finv[n - m] % P * Finv[m] % P; }
卢卡斯定理
现在有了新问题,如果$n$和$m$非常大,$p$为素数,比如求$C_n^m \% p \ ,\ n\leqslant 10^{18},m\leqslant 10^{18},p\leqslant 10^{9}$
$C_n^m\ \%\ p = C(n / p, m / p) * C(n\ \%\ p, m\ \%\ p)\ \%\ p$
或者写成这样更准确$Lucas(n,m)\ \%\ p=Lucas(n/p,m/p)*C(n\ \%\ p,m\ \%\ p)\ \%\ p$
证明请看此 lucas_百度百科,没仔细看证明,所以对不对我也不知道。
写成递归,代码就这么短:
LL Lucas(LL n, LL m, int p){ return m ? Lucas(n/p, m/p, p) * C(n%p, m%p, p) % p : 1; }
具体C的实现要看情况。
P较小时,打表
typedef long long ll; const int N = 1e5 ; const int P = 99991;//取一个小于N的素数 ll fact[P + 5], inv[P + 5], Finv[P + 5];//阶乘打表 void init() { inv[1] = 1; //线性递推求逆元 for (int i = 2; i <= P; i++) { inv[i] = (P - P / i) * 1ll * inv[P % i] % P; } fact[0] = Finv[0] = 1; for (int i = 1; i < P; i++) { fact[i] = fact[i - 1] * 1ll * i % P;//求阶乘 Finv[i] = Finv[i - 1] * 1ll * inv[i] % P;//求阶乘的逆元 } } ll C(ll n, ll m){//组合数C(n, m) % p if (m > n)return 0; return fact[n] * Finv[n - m] % P * Finv[m] % P; } ll Lucas(ll n, ll m){ return m ? C(n % P, m % P) * Lucas(n / P, m / P) % P : 1; }
P较大时,没法打表,用快速幂算逆元
typedef long long ll; const int N = 1e9 ; const int P = 1e8 + 7; ll quickPower(ll a, ll b) { ll res = 1; a %= P; while (b) { if (b & 1)res = (res % P) * (a % P) % P; a = (a % P) * (a % P) % P; b >>= 1; } return res; } ll inv(ll x) {//x关于p的逆元,p为素数 return quickPower(x, P - 2); } ll C(ll n, ll m) { if (m > n)return 0; ll up = 1, down = 1;//分子分母; for (int i = n - m + 1; i <= n; i++) up = up * i % P; for (int i = 1; i <= m; i++) down = down * i % P; return up * inv(down) % P; } ll Lucas(ll n, ll m) { if (m == 0)return 1; return C(n % P, m % P) * Lucas(n / P, m / P) % P; }