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

#概述
首先我们要知道什么是组合数。具体可以参考我之前的博客 “排列与组合”笔记 中,集合的组合的部分。

这里复述如下: 令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 − 1 , k − 1 ) + C ( n − 1 , k ) C ( n , 0 ) =   C ( n , n ) = 1 \left\{ \begin{aligned} C(n, k) = &\ C(n - 1, k - 1) + C(n - 1, k) \\ C(n, 0) = &\ C(n, n) = 1 \end{aligned} \right. {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 , k ) C(n, k) C(n,k) 的值即可。

当然我们知道 C ( n , k ) = C ( n , n − k ) C(n, k) = C(n, n - k) C(n,k)=C(n,nk) ,所以上面的代码有很多空间和时间的浪费。可以将 tC[][] 二维数组转化为一维数组存储,同时,当 j &gt; i / 2 j &gt; i / 2 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) C(n,k) 只需要返回内联函数 C ( n , k ) C(n, k) C(n,k) 的值即可。

显然,由于空间的限制,pascal打表的方式并不适合求取一些比较大的组合数。例如,我们现在要求取的组合数的 n n n 的范围是 [ 1 , 1000000 ] [1, 1000 000] [1,1000000] , 那么我们应该怎么办呢? 那就轮到方法二大显身手了。
#法二:逆元求取组合数
由定理可知:如果用C(n, r)表示n-元素集的r-组合的个数,有 C ( n , r ) = n ! r ! ∗ ( n − r ) ! C(n, r) = \frac {n!} {r! * (n - r)!} C(n,r)=r!(nr)!n!

而我们的目标就是计算 C ( n , r ) % m o d C(n, r) \% mod 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 ≠ \neq ̸= (a % p / b % p) % p 。

显然数学家们是不能忍受这种局面的,他们扔出了**“逆元”**来解决这个问题。那么什么是逆元? 逆元和模运算中的除法又有什么关系呢?

首先给出数论中的解释:

对于正整数 a a a p p p,如果有 a x ≡ 1 ( m o d p ) ax \equiv 1 ( mod p) ax1(modp),那么把这个同余方程中 x x x 的最小正整数解叫做 a a a p p p 的逆元。

什么意思呢? 就是指,如果 a x % p = 1 ax \% p = 1 ax%p=1 , 那么x的最小正整数解就是 a a a 的逆元。

现在我们来解决模运算的除法问题。假设我们要计算的是 a b % p \frac a b \% p ba%p的值 m m m,即 a b % p = m      ( a 和 b 满 足 a % b = 0 ) \frac a b \% p = m \ \ \ \ (a和b满足 a \% b = 0) ba%p=m    (aba%b=0)

同时存在另一个数 x x x 满足 a x % p = m ax \% p = m ax%p=m

由模运算对乘法成立,两边同时乘以 b b b ,得到: a % p = ( m ( b % p ) ) % p a \% p = (m(b \% p)) \% p a%p=(m(b%p))%p

如果 a a a b b b 均小于模数 p p p 的话,上式可以改写为: a = b m % p a = bm \% p a=bm%p

等式两边再同时乘以 x x x, 得到: a x % p = m % p = x b m % p ax \% p = m \% p = xbm \% p ax%p=m%p=xbm%p

因此可以得到: b x % p = 1 bx \% p = 1 bx%p=1

哎,x是b的逆元呀(x 在模运算的乘法中等同于 1 b \frac 1 b b1, 这就是逆元的意义)

由以上过程我们看到,求取 ( a b % p ) (\frac a b \% p) (ba%p) 等同于 求取 ( a ∗ ( b 的 逆 元 ) % p ) (a * (b的逆元) \% p) (a(b)%p) 。 因此,求模运算的除法问题就转化为就一个数的逆元问题了。

而求取一个数的逆元,有两种方法

  1. 拓展欧几里得算法

  2. 费马小定理

对于利用拓展欧几里得算法求逆元,很显然,如果 b x % p = 1 bx \% p = 1 bx%p=1,那么 b x + p y = 1 bx + py = 1 bx+py=1, 直接利用 exgcd(b, p, x, y)(代码实现在后面给出),则 ( x % p + p ) % p (x \% p+ p) \% p (x%p+p)%p 即为 b b b 的逆元。

对于第二种方法,因为在算法竞赛中模数p总是质数,所以可以利用费马小定理 b p − 1 % p = 1 b^{p-1} \% p = 1 bp1%p=1

可以直接得到 b b b 的逆元是 b p − 2 b^{p-2} bp2 , 使用 快速幂 求解即可。

明白了以上几个关键点,那么求取组合数 C ( n , r ) C(n, r) C(n,r) 的算法就呼之欲出了:

  1. 求取1到n的阶乘对 mod 取模的结果存入数组 JC[] 中;
  2. 求取 C ( n , r ) C(n, r) C(n,r) 时, 先利用“拓展欧几里得算法”或者“费马小定理+快速幂”求 JC[r]的逆元存入临时变量 x 1 x_1 x1 ;
  3. 然后计算 J C [ n ] ∗ x 1 % m o d JC[n] * x_1 \% mod JC[n]x1%mod 存入临时变量 x 2 x_2 x2;( x 2 x_2 x2 即为 n ! r ! % m o d \frac {n!} {r!} \% mod r!n!%mod 的值)
  4. 求取JC[n - r] 的逆元存入临时变量 x 3 x_3 x3;
  5. 则可以得到 C ( n , r ) = x 2 ∗ x 3 % m o d C(n, r) = {x_2} * {x_3} \% mod C(n,r)=x2x3%mod

下面是方法二的代码片段:

typedef long long LL;
const LL maxn(1000005), mod(1e9 + 7);
LL Jc[maxn];

void calJc()	//求maxn以内的数的阶乘
{
    Jc[0] = Jc[1] = 1;
    for(LL i = 2; i < maxn; i++)
        Jc[i] = Jc[i - 1] * i % mod;
}
/*
//拓展欧几里得算法求逆元
void exgcd(LL a, LL b, LL &x, LL &y)	//拓展欧几里得算法
{
    if(!b) x = 1, y = 0;
    else
    {
        exgcd(b, a % b, y, x);
        y -= x * (a / b);
    }
}

LL niYuan(LL a, LL b)	//求a对b取模的逆元
{
    LL x, y;
    exgcd(a, b, x, y);
    return (x + b) % b;
}
*/

//费马小定理求逆元
LL pow(LL a, LL n, LL p)	//快速幂 a^n % p
{
    LL ans = 1;
    while(n)
    {
        if(n & 1) ans = ans * a % p;
        a = a * a % p;
        n >>= 1;
    }
    return ans;
}

LL niYuan(LL a, LL b)	//费马小定理求逆元
{
    return pow(a, b - 2, b);
}

LL C(LL a, LL b)	//计算C(a, b)
{
    if(a < b) return 0;
    return Jc[a] * niYuan(Jc[b], mod) % mod
        * niYuan(Jc[a - b], mod) % mod;
}

以上即为逆元求取组合数的方法,无论使用拓展欧几里得还是费马小定理,一开始求取Jc数组是的复杂度是 O ( n ) O(n) O(n),拓展欧几里得算法和费马小定理的复杂度均为 O ( l g ( m o d ) ) O(lg(mod)) O(lg(mod)) , 如果要求取m次组合数,则总的时间复杂度为 O ( m n l g ( m o d ) ) O(mnlg(mod)) O(mnlg(mod)).

以上です。

<think>我们参考了用户提供的多个引用,其中引用[2][3][4]都展示了如何通过阶乘函数来计算组合数组合数的公式为:C(m, n) = m! / (n! * (m-n)!) 根据用户的需,他想要一个组合数的函数。我们可以提供几种不同的方法,包括: 1. 直接使用阶乘函数计算(适用于小范围,因为阶乘增长很快,m<=12左右) 2. 递推方法(如杨辉三角,可以预处理组合数,适用于多次查询) 3. 使用公式变形减少计算量(例如,循环计算时分子分母分别计算并约分) 4. 使用逆元预处理(适用于质数的大组合数计算) 不过用户提供的引用中,问题规较小(m<=12),所以我们可以采用第一种方法,即先写一个阶乘函数,然后用组合数公式计算。 但是,用户要的是“函数”,我们可以提供几种不同的函数实现。 考虑到用户可能需要在不同场景下使用,我们提供三种常见的组合数方法方法一:直接计算阶乘(适合小数据) 方法二:递推法(动态规划,适合中等数据,如n,m<=1000) 方法三:使用逆元(适合大组合数,需要数为质数) 由于用户引用中提到了多个方法,并且用户之前的问题是关于C语言的,我们将给出C语言实现的函数。 注意:用户引用[5]提到了Lucas定理和预处理逆元的方法,但用户的问题规可能不需要,这里我们根据需提供。 根据用户样例输入(5,3)输出10,我们设计函数。 我们首先提供方法一:阶乘函数法(适合m<=12,因为12!在int范围内,13!会溢出) 函数原型:int C(int m, int n); 方法二:递推法(动态规划),利用公式:C(m, n) = C(m-1, n) + C(m-1, n-1) 并设置边界条件。 方法三:适用于大组合数(这里用户没有要,但为了完整性,我们给出质数的情况,例如1000000007) 由于用户没有明确要,我们重点实现方法一和方法二。 我们按照以下步骤: 1. 定义阶乘函数factorial,用于方法一。 2. 在组合数函数C1中使用阶乘函数计算。 3. 实现递推法的组合数函数C2,使用二维数组存储中间结果(注意:需要先确定最大范围,比如m<=1000)。 但是,用户要的是函数,所以我们提供两个版本的函数,并说明使用场景。 注意:用户引用的代码中,有的使用了double类型来避免整数溢出,但题目要m<=12,所以用int即可。 下面我们具体实现: </think>在C语言中,组合数的函数实现主要有以下几种方法,各有适用场景: ### 方法一:阶乘公式法(适合小范围) ```c // 计算阶乘 int factorial(int n) { if (n <= 1) return 1; int result = 1; for(int i = 2; i <= n; i++) { result *= i; } return result; } // 组合数函数 C(m,k) = m!/(k!*(m-k)!) int combination(int m, int k) { if(k < 0 || k > m) return 0; // 非法输入处理 return factorial(m) / (factorial(k) * factorial(m - k)); } ``` **特点**: - 时间复杂度:$O(m)$,其中阶乘计算耗时 - 适用:$m \leq 12$(因13! > INT_MAX会溢出) - 示例调用:`combination(5,3)` 返回 `10`[^2][^4] ### 方法二:递推法(动态规划) ```c #define MAX_N 1000 int dp[MAX_N][MAX_N]; // 记忆化数组 void init_combination() { for(int i = 0; i < MAX_N; i++) { dp[i][0] = 1; // C(i,0)=1 for(int j = 1; j <= i; j++) { dp[i][j] = dp[i-1][j] + dp[i-1][j-1]; // 递推公式 } } } int combination(int m, int k) { if(k < 0 || k > m) return 0; return dp[m][k]; } ``` **特点**: - 时间复杂度:预处理$O(n^2)$,查询$O(1)$ - 适用:多次查询$m,k \leq 1000$ - 原理:杨辉三角递推 $C(m,k) = C(m-1,k) + C(m-1,k-1)$[^1] ### 方法三:乘除交替法(优化计算) ```c int combination(int m, int k) { if(k == 0 || k == m) return 1; k = (k < m-k) ? k : m-k; // 较小值优化计算 long result = 1; for(int i = 1; i <= k; i++) { result = result * (m - i + 1) / i; // 乘除交替避免溢出 } return (int)result; } ``` **特点**: - 时间复杂度:$O(k)$ - 适用:$m \leq 62$(保证不溢出long范围) - 优势:避免大数阶乘,计算路径最优[^3] ### 选择建议 | 方法 | 最佳适用范围 | 时间复杂度 | 优点 | |------------|------------------|----------------|------------------------| | 阶乘公式 | $m \leq 12$ | $O(m)$ | 实现简单 | | 递推法 | $m \leq 1000$ | 预处理$O(n^2)$ | 适合多次查询 | | 乘除交替 | $m \leq 62$ | $O(k)$ | 计算高效,优化溢出风险 | > **重要提示**:当$m>62$时,需改用**大数运算**或**组合数**(如Lucas定理)[^5]
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值