题解:AcWing 887 求组合数III

【题目来源】

AcWing:887 求组合数 III - AcWing题库

【题目描述】

给定 \(n\) 组询问,每组询问给定三个整数 \(a,b,p\),其中 \(p\) 是质数,请你输出 \(C_a^b\ mod\ p\) 的值。

【输入】

第一行包含整数 \(n\)

接下来 \(n\) 行,每行包含一组 \(a,b,p\)

【输出】

\(n\) 行,每行输出一个询问的解。

【输入样例】

3
5 3 7
3 1 5
6 4 13

【输出样例】

3
3
2

【核心思想】

  1. 问题分析:给定 \(n\) 组询问,每组给定三个整数 \(a, b, p\)\(p\) 为质数),要求计算组合数 \(C_a^b \bmod p\) 的值。数据范围中 \(a, b\) 可能极大(如 \(10^{18}\)),远超模数 \(p\) 的规模,无法直接使用阶乘预处理或递推法,需要利用卢卡斯定理(Lucas Theorem)将大问题分解为 \(p\) 进制下的多个小组合数问题。

  2. 算法选择

    • 卢卡斯定理:对于质数 \(p\),有 \(C(a, b) \equiv C(a \bmod p, b \bmod p) \cdot C(\lfloor a/p \rfloor, \lfloor b/p \rfloor) \pmod p\)。该定理将大组合数模质数的问题递归分解为多个小组合数(参数均小于 \(p\))的乘积
    • 递归实现:不断将 \(a, b\) 除以 \(p\),直到 \(b = 0\)(边界 \(C(a, 0) = 1\)
    • 小组合数计算:由于 \(a \bmod p < p\),无法预处理阶乘到 \(a\) 的规模,但可以直接用乘法公式 + 费马小定理求逆元\(O(p)\) 时间内计算单个 \(C(x, y)\)(其中 \(x, y < p\)
  3. 关键步骤

    • 快速幂求逆元(费马小定理):qmi(x, p-2) 计算 \(x^{-1} \bmod p\)
    • 计算小组合数 \(C(a, b)\)(要求 \(a, b < p\)):
      • \(b > a\),返回 \(0\)
      • 利用对称性 \(C(a, b) = C(a, a-b)\),令 \(b = \min(b, a-b)\)
      • 分子连乘:\(res = a \cdot (a-1) \cdot \ldots \cdot (a-b+1) \bmod p\)
      • 分母逐项求逆元并乘入:\(res = res \cdot i^{-1} \bmod p\)\(i = 1, 2, \ldots, b\)
      • 返回 \(res\)
    • 卢卡斯递归
      • 边界:若 \(b = 0\),返回 \(1\)
      • \(a < p\)\(b < p\),直接返回 \(C(a, b)\)
      • 否则返回 \(C(a \% p, b \% p) \cdot \text{lucas}(a/p, b/p) \bmod p\)
    • 处理查询:读取 \(n\)\((a, b, p)\),调用 lucas(a, b) 输出结果
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \cdot p \cdot \log_p a)\)。每次 lucas 递归深度为 \(O(\log_p a)\),每层计算 \(C(a \% p, b \% p)\) 需要 \(O(p)\) 时间
    • 空间复杂度:\(O(\log_p a)\),递归栈深度
  5. 卢卡斯定理的核心思想

    • \(p\) 进制分解:将 \(a, b\) 表示为 \(p\) 进制数:\(a = a_k p^k + \ldots + a_1 p + a_0\)\(b = b_k p^k + \ldots + b_1 p + b_0\)。卢卡斯定理等价于 \(C(a, b) \equiv \prod_{i=0}^{k} C(a_i, b_i) \pmod p\),即 \(p\) 进制下各位独立计算后相乘
    • 降维打击:将参数为 \(10^{18}\) 级别的大组合数问题,转化为 \(O(\log_p a)\) 个参数小于 \(p\) 的小组合数问题
    • 适用条件:卢卡斯定理仅当模数 \(p\) 为质数时成立,若 \(p\) 为合数需要使用扩展卢卡斯定理(ExLucas)
    • 小组合数计算策略:当 \(p\) 较小(如 \(p \leq 10^5\))时,\(O(p)\) 直接计算小组合数是可接受的;若 \(p\) 也较大,则需预处理 \([0, p)\) 的阶乘和逆元
    • 适用于大组合数模小质数、组合数取模中模数为质数且被组合数参数远超模数的场景

【算法标签】

排列组合

【代码详解】

#include <bits/stdc++.h>
using namespace std;

#define int long long  // 使用long long类型防止溢出
int p;  // 模数,必须是质数

// 快速幂算法,计算 a^b mod p
int qmi(int a, int b)
{
    int mul = 1;  // 结果初始化为1

    while (b)  // 当指数b不为0时继续
    {
        if (b & 1)  // 如果b的最低位是1
        {
            mul = mul * a % p;  // 将当前a乘入结果
        }
        a = a * a % p;  // a自乘
        b >>= 1;        // b右移一位
    }

    return mul;  // 返回a^b mod p
}

// 直接计算组合数 C(a, b) mod p,要求 a,b < p
int C(int a, int b)
{
    if (b > a) return 0;  // 如果b>a,组合数为0
    if (b > a - b) b = a - b;  // 使用对称性 C(a,b)=C(a,a-b),减少计算量

    int res = 1;  // 结果初始化为1

    // 计算 C(a, b) = a! / (b! * (a-b)!) = a*(a-1)*...*(a-b+1) / b!
    for (int i = 1, j = a; i <= b; i++, j--)
    {
        // 分子:j = a, a-1, ..., a-b+1
        res = res * j % p;

        // 除以分母的第i项i
        // 费马小定理:i^{-1} ≡ i^{p-2} (mod p)
        res = res * qmi(i, p - 2) % p;
    }

    return res;  // 返回 C(a, b) mod p
}

// 卢卡斯定理递归计算组合数
int lucas(int a, int b)
{
    if (b == 0) return 1;  // 基本情况:C(a, 0) = 1

    // 如果a,b都小于p,直接计算
    if (a < p && b < p)
    {
        return C(a, b);
    }

    // 卢卡斯定理:C(a, b) ≡ C(a mod p, b mod p) * C(a/p, b/p) (mod p)
    return C(a % p, b % p) * lucas(a / p, b / p) % p;
}

signed main()  // 因为使用了#define int long long,所以用signed main
{
    int n;  // 查询次数
    cin >> n;

    while (n--)  // 处理每个查询
    {
        int a, b;  // 输入C(a, b)
        cin >> a >> b >> p;  // 输入a, b和模数p

        // 使用卢卡斯定理计算组合数
        cout << lucas(a, b) << endl;
    }

    return 0;
}

【运行结果】

3
5 3 7
3
3 1 5
3
6 4 13
2
posted @ 2026-06-23 11:24  团爸讲算法  阅读(9)  评论(0)    收藏  举报