求组合数(取模)的两种方法

2019-04-13 17:34发布

概述

首先我们要知道什么是组合数。具体可以参考我之前的博客 “排列与组合”笔记 中,集合的组合的部分。 这里复述如下: 令r为非负整数。我们把n个元素的集合S的r-组合理解为从S的n个元素中对r个元素的无序选择。换句话说,S的一个r-组合是S的一个子集,该子集由S的n个元素中的r个组成,即S的一个r-元素子集。 由此,求解组合数即变成了求式子C(n, r) 的值。

法一:Pascal公式打表

由Pascal公式(参考 组合数学笔记之二——二项式系数),我们知道
{C(n,k)=C(n,0)= C(n1,k1)+C(n1,k) C(n,n)=1
取二维数组 tC[][] ,初始化 tC[0][0] = 1; 打表即可。代码最简单,如下: const int maxn(1005), mod(100003); int tC[maxn][maxn]; //tC 表示 table of C inline int C(int n, int k) { if(k > n) return 0; return tC[n][k]; } void calcC(int n) { for(int i = 0; i < n; i++) { tC[i][0] = 1; for(int j = 1; j < i; j++) tC[i][j] = (C(i - 1, j - 1) + C(i - 1, j)) % mod; tC[i][i] = 1; } } 计算 C(n,k) 返回内联函数C(n,k) 的值即可。 当然我们知道 C(n,k)=C(n,nk) ,所以上面的代码有很多空间和时间的浪费。可以将 tC[][] 二维数组转化为一维数组存储,同时,当 j>i/2 时终止第二层循环,新代码如下: const int maxn(10005), mod(100003); int tC[maxn * maxn]; //tC 表示 table of C inline int loc(int n, int k) // C(n, k)返回在一维数组中的位置 { int locate = (1 + (n >> 1)) * (n >> 1); // (n >> 1) 等价于 (n / 2) locate += k; locate += (n & 1) ? (n + 1) >> 1 : 0; // (n & 1) 判断n是否为奇数 return locate; } inline int C(int n, int k) { if(k > n) return 0; k = min(n - k, k); return tC[loc(n, k)]; } void calcC(int n) { for(int i = 0; i < n; i++) { tC[loc(i, 0)] = 1; for(int j = 1, e = i >> 1; j <= e; j++) tC[loc(i, j)] = C(i - 1, j) + C(i - 1, j - 1); } } 同样,要得到 C(n,k) 只需要返回内联函数C(n,k) 的值即可。 显然,由于空间的限制,pascal打表的方式并不适合求取一些比较大的组合数。例如,我们现在要求取的组合数的 n 的范围是 [1,1000000] , 那么我们应该怎么办呢? 那就轮到方法二大显身手了。

法二:逆元求取组合数

由定理可知:如果用C(n, r)表示n-元素集的r-组合的个数,有C(n,r)=n!r!(nr)! 而我们的目标就是计算 C(n,r)%mod 的值。 由数论的知识我们知道,模运算的加法,减法,乘法和四则运算类似,即:
模运算与基本四则运算有些相似,但是除法例外。其规则如下:
  • (a + b) % p = (a % p + b % p) % p
  • (a - b) % p = (a % p - b % p) % p
  • (a * b) % p = (a % p * b % p) % p
但对于除法却不成立,即(a / b) % p (a % p / b % p) % p 。 显然数学家们是不能忍受这种局面的,他们扔出了“逆元”来解决这个问题。那么什么是逆元? 逆元和模运算中的除法又有说明关系呢? 首先给出数论中的解释:
对于正整数 ap,如果有 ax1(modp),那么把这个同余方程中 x 的最小正整数解叫做 ap 的逆元。
什么意思呢? 就是指,如果 ax%p=1 , 那么x的最小正整数解就是 a 的逆元。 现在我们来解决模运算的除法问题。假设 ab 同时存在另一个数 x 满足ax%p=m 由模运算对乘法成立,两边同时乘以 b ,得到:a%p=(m(b%p))%p 如果 ab 均小于模数 p 的话,上式可以改写为:a=bm%p 等式两边再同时乘以 x, 得到: ax%p=m%p=xbm%p 因此可以得到: bx%p=1 哎,x是b的逆元呀(x 在模运算的乘法中等同于 1b, 这就是逆元的意义) 由以上过程我们看到,求取 (ab%p) 等同于 求取 (a(b)%p) 。 因此,求模运算的除法问题就转化为就一个数的逆元问题了。 而求取一个数的逆元,有两种方法
  1. 拓展欧几里得算法
  2. 费马小定理
对于利用拓展欧几里得算法求逆元,很显然,如果bx%p=1,那么 bx+py=1, 直接利用 exgcd(b, p, x, y)(代码实现在后面给出),则 (x%p+p)%p 即为