题解:AcWing 888 求组合数IV
【题目来源】
AcWing:888 求组合数 IV - AcWing题库
【题目描述】
输入 \(a,b\),求 \(C_a^b\) 的值。
注意结果可能很大,需要使用高精度计算。
【输入】
共一行,包含两个整数 \(a\) 和 \(b\)。
【输出】
共一行,输出 \(C_a^b\) 的值。
【输入样例】
5 3
【输出样例】
10
【核心思想】
-
问题分析:给定两个整数 \(a, b\),求组合数 \(C_a^b\) 的精确值(不取模)。由于结果可能极大(如 \(C_{1000}^{500}\) 有 300 位左右),远超 64 位整数范围,不能直接使用阶乘相除计算,需要借助质因数分解 + 高精度乘法来精确求解。
-
算法选择:
- 线性筛法(欧拉筛):预处理 \([2, a]\) 范围内的所有质数,时间复杂度 \(O(a)\),每个合数仅被其最小质因子筛去一次
- 勒让德定理(Legendre's Formula):计算 \(n!\) 中质因子 \(p\) 的指数:\(\sum_{k=1}^{\infty} \lfloor n/p^k \rfloor\)。该定理避免了直接分解巨大的阶乘值
- 质因数分解求组合数:\(C(a, b) = \frac{a!}{b!(a-b)!}\),对于每个质数 \(p\),其在 \(C(a, b)\) 中的指数为 \(\text{get}(a, p) - \text{get}(b, p) - \text{get}(a-b, p)\)
- 高精度乘法(大整数 × 小整数):将分解得到的所有质因子按指数乘起来,得到最终的精确值
-
关键步骤:
- 筛质数:调用
get_primes(a),用线性筛法求出 \([2, a]\) 的所有质数,存入primes[] - 质因数分解(对每个质数 \(p_i\)):
- 利用勒让德定理计算 \(n!\) 中 \(p\) 的指数:
get(n, p)- 初始化
res = 0 - 循环:
res += n / p,n /= p,直到 \(n = 0\)
- 初始化
- \(C(a, b)\) 中 \(p\) 的指数:
sum[i] = get(a, p) - get(b, p) - get(a-b, p)
- 利用勒让德定理计算 \(n!\) 中 \(p\) 的指数:
- 高精度乘法计算结果:
- 初始化结果
res = [1](高精度整数,低位在前) - 对于每个质数 \(p_i\) 及其指数 \(exp\):
- 将
res连续乘 \(p_i\) 共 \(exp\) 次:res = mul(res, p_i)
- 将
mul(A, b)实现大整数 \(A\) 乘小整数 \(b\):- 遍历 \(A\) 的每一位(或还有进位),
t += A[i] * b - 结果位
t % 10,新进位t / 10 - 去除前导零
- 遍历 \(A\) 的每一位(或还有进位),
- 初始化结果
- 输出结果:从高位到低位输出
res
- 筛质数:调用
-
时间/空间复杂度:
- 时间复杂度:\(O(a \log a + D \cdot \pi(a))\),其中 \(D\) 为结果总位数。线性筛 \(O(a)\);对每个质数计算指数 \(O(\log a)\),共 \(\pi(a)\) 个质数;高精度乘法总代价与结果位数和质因子个数相关
- 空间复杂度:\(O(a + D)\),存储质数表和结果数组
-
质因数分解法求组合数的核心思想:
- 绕过直接阶乘:\(a!\) 的数值极其巨大,直接计算不可行。通过质因数分解,将问题转化为对每个小质数计算指数,再相乘得到结果
- 勒让德定理的妙用:\(n!\) 中质因子 \(p\) 的指数不需要知道 \(n!\) 的具体值,只需不断除以 \(p\) 累加即可。这是因为 \(1 \sim n\) 中 \(p\) 的倍数贡献至少一个 \(p\),\(p^2\) 的倍数额外再贡献一个,以此类推
- 高精度乘小整数:相比于高精度乘高精度,高精度乘小整数的实现更简单且效率更高(\(O(D)\))。由于最终结果是多个小质数的乘积,每次只需乘一个小整数
- 与其他方法的对比:
- 递推法(885):\(O(a^2)\),仅适合 \(a \leq 2000\) 且结果在模意义下
- 阶乘逆元法(886):\(O(a \log p)\),需要模数为质数
- 卢卡斯定理(887):适合 \(a\) 极大但模数 \(p\) 较小且为质数
- 质因数分解 + 高精度(888):唯一能在 \(a \leq 5000\) 范围内求出精确值的方法
- 适用于组合数精确值计算、结果超出任何内置整数范围、但参数规模适中(\(a \leq 10^4\) 量级)的场景
【算法标签】
排列组合
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 5005; // 质数表大小
int primes[N], cnt; // primes存储质数,cnt是质数个数
bool st[N]; // 筛法标记数组
int sum[N]; // 存储每个质因子的指数
// 线性筛法求1~n的所有质数
void get_primes(int n)
{
for (int i = 2; i <= n; i++)
{
if (!st[i]) // 如果i是质数
{
primes[cnt++] = i; // 加入质数表
}
// 用当前已得到的质数筛掉合数
for (int j = 0; primes[j] <= n / i; j++)
{
st[primes[j] * i] = true; // 标记合数
if (i % primes[j] == 0) // 如果primes[j]是i的最小质因数
{
break; // 保证每个合数只被最小的质因数筛掉
}
}
}
}
// 计算n!中质因子p的指数(勒让德定理)
int get(int n, int p)
{
int res = 0; // 指数结果
// 勒让德公式:n!中质因子p的指数 = ⌊n/p⌋ + ⌊n/p²⌋ + ⌊n/p³⌋ + ...
while (n)
{
res += n / p; // 计算⌊n/p⌋
n /= p; // 继续计算下一项
}
return res;
}
// 高精度乘法:大整数A乘以小整数b
vector<int> mul(vector<int> &A, int b)
{
vector<int> C; // 存储结果
int t = 0; // 进位
for (int i = 0; i < A.size() || t; i++) // 遍历A的每一位或还有进位
{
if (i < A.size()) // 如果A还有位数
{
t += A[i] * b; // 当前位乘以b加上进位
}
C.push_back(t % 10); // 取个位
t /= 10; // 计算进位
}
// 去掉前导0,但保留最后一位0(如果结果就是0)
while (C.size() > 1 && C.back() == 0)
{
C.pop_back();
}
return C;
}
int main()
{
int a, b; // 输入C(a, b)
cin >> a >> b;
// 1. 筛出1~a的所有质数
get_primes(a);
// 2. 计算组合数C(a,b)的质因数分解
for (int i = 0; i < cnt; i++)
{
int p = primes[i]; // 当前质数
// 勒让德公式计算C(a,b)中质因子p的指数
// C(a,b) = a! / (b! * (a-b)!)
// 指数 = 在a!中的指数 - 在b!中的指数 - 在(a-b)!中的指数
sum[i] = get(a, p) - get(b, p) - get(a - b, p);
}
// 3. 用高精度乘法计算结果
vector<int> res; // 存储高精度结果,低位在前
res.push_back(1); // 初始化为1
// 遍历所有质因子
for (int i = 0; i < cnt; i++)
{
int p = primes[i]; // 当前质数
int exp = sum[i]; // 质因子p的指数
// 将p乘exp次
for (int j = 0; j < exp; j++)
{
res = mul(res, p);
}
}
// 4. 输出结果(从高位到低位)
for (int i = res.size() - 1; i >= 0; i--)
{
cout << res[i];
}
cout << endl;
return 0;
}
【运行结果】
5 3
10
浙公网安备 33010602011771号