P2480 [SDOI2010] 古代猪文 解题报告
解题报告:P2480 [SDOI2010] 古代猪文
大家好,今天我们来攻克一道非常经典的数论题——“古代猪文”。这道题就像一个“数论知识全家桶”,需要我们巧妙地组合运用欧拉定理、卢卡斯定理和中国剩余定理。下面,我将带大家一步步拆解这道题。
第一步:读懂题目,化繁为简
冗长的背景故事可以先放一边,我们直接提取核心的数学问题。
- 研究对象:远古猪文有 \(n\) 个字。
- 研究条件:某个朝代的字数是总数 \(n\) 的 \(1/k\),其中 \(k\) 是 \(n\) 的一个正约数。
- 情况总数:对于一个确定的 \(k\),字数是 \(n/k\)。从 \(n\) 个字中选出 \(n/k\) 个字,有多少种选法?这是一个组合问题,答案是 \(C_n^{n/k}\)。
- 总代价:iPig 考虑了所有可能的 \(k\)。设 \(p\) 是所有可能情况的总和。我们需要计算 \(g^p\) 对 \(999911659\) 取模的结果。
让我们来梳理一下 \(p\) 到底是什么。\(k\) 是 \(n\) 的所有正约数。如果我们令 \(d = n/k\),那么当 \(k\) 取遍 \(n\) 的所有约数时,\(d\) 也同样会取遍 \(n\) 的所有约数。
因此,总情况数 \(p\) 就是对所有 \(n\) 的约数 \(d\),求 \(C_n^d\) 的和。
最终,我们需要计算的目标就是:
第二步:处理巨大的指数 —— 欧拉定理
这个算式的指数部分是一个非常大的求和,直接计算再求幂是绝对不可能的。当我们遇到 \(a^b \pmod m\) 这种形式,并且指数 \(b\) 很大时,第一反应就应该是欧拉定理(或其特例费马小定理)。
欧拉定理告诉我们:如果 \(a\) 和 \(m\) 互质,则 \(a^{\phi(m)} \equiv 1 \pmod m\)。
由此可以推导出降幂公式:\(a^b \equiv a^{b \pmod{\phi(m)}} \pmod m\)。
(一个更严谨的扩展欧拉定理可以处理不互质的情况,但这里用不到,我们暂时忽略)。
题目中的模数 \(P = 999911659\) 是一个质数(可以自行验证或相信出题人)。对于质数 \(P\),它的欧拉函数 \(\phi(P) = P-1\)。
所以,我们可以使用费马小定理(欧拉定理在模数为质数时的特例)来降幂。
前提是 \(g\) 不能是 \(P\) 的倍数。如果 \(g\) 是 \(P\) 的倍数,那么 \(g^p \pmod P\) 的结果显然是 0。我们可以特判一下这种情况。
于是,我们的问题转化为了计算指数部分对 \(P-1\) 取模的值。
新的目标是计算:
计算出 \(S\) 之后,最终答案就是 \(g^S \pmod{999911659}\)。
第三步:处理复合模数的组合数 —— 中国剩余定理 (CRT)
现在,我们的核心任务是计算 \(S = \left( \sum_{d|n} C_n^d \right) \pmod{999911658}\)。
新的模数 \(M = 999911658\) 是一个合数。计算组合数对合数取模是一个难题。直接计算 \(C_n^d\) 会非常大,而常规的求组合数模质数的方法(比如利用逆元)在这里也不适用,因为求逆元需要模数是质数。
怎么办呢?我们可以把这个“大”问题分解成几个“小”问题。这就是中国剩余定理 (CRT) 的用武之地。
CRT的核心思想是:如果我们要计算一个数模 \(M\) 的结果,而 \(M\) 可以被分解为几个两两互质的数的乘积 \(M = p_1 \times p_2 \times \dots \times p_k\),我们可以:
- 分别计算这个数模 \(p_1, p_2, \dots, p_k\) 的结果,得到 \(a_1, a_2, \dots, a_k\)。
- 然后解一个同余方程组:\[\begin{cases} x \equiv a_1 \pmod{p_1} \\ x \equiv a_2 \pmod{p_2} \\ \vdots \\ x \equiv a_k \pmod{p_k} \end{cases} \]
- 这个方程组在模 \(M\) 下的唯一解 \(x\),就是我们想要的答案。
我们先对 \(M=999911658\) 进行质因数分解:
这四个质因数两两互质,完美符合CRT的应用条件!
所以,我们的计划是:
- 分别计算 \(S\) 对 \(p_1=2, p_2=3, p_3=4679, p_4=35617\) 取模的结果。
- 使用CRT将这四个结果合并,得到最终的 \(S\)。
第四步:计算组合数模小质数 —— 卢卡斯定理
现在,问题被拆解为4个独立的子任务,每个任务的形式都是:
计算 \(\left( \sum_{d|n} C_n^d \right) \pmod p\),其中 \(p\) 是一个小质数(例如 \(2, 3, 4679, 35617\))。
根据模运算的性质,这等价于:
我们需要一个能快速计算 \(C_n^d \pmod p\) 的方法,其中 \(n,d\) 可能很大,但 \(p\) 相对较小。这正是卢卡斯定理 (Lucas's Theorem) 的主场!
卢卡斯定理 提供了一个递归公式:
其中 /
表示整除。
这个公式的含义是,我们可以把 \(n\) 和 \(m\) 看作 \(p\) 进制数,然后对每一位分别求组合数,最后乘起来。递归的终点是当 \(m=0\) 时, \(C_n^0 = 1\)。
递归的“基本步”是计算 \(C_{n \pmod p}^{m \pmod p} \pmod p\)。因为 \(n \pmod p\) 和 \(m \pmod p\) 都小于 \(p\),我们可以用预处理阶乘和阶乘逆元的方式 \(O(1)\) 算出。
所以,对于每一个质因子 \(p_i\),我们的计算流程是:
- 预处理出 \(0!\) 到 \((p_i-1)!\) 的阶乘及其模 \(p_i\) 的逆元。
- 找出 \(n\) 的所有约数 \(d\)。
- 对于每个约数 \(d\),使用卢卡斯定理计算 \(C_n^d \pmod{p_i}\)。
- 将所有这些结果加起来,再模 \(p_i\),就得到了同余方程组中的一个余数 \(a_i\)。
总结与回顾
我们的解题路径像一个层层递进的探险:
- 顶层问题:求 \(G^{\sum C_n^d} \pmod{P}\)。
- 第一层(降幂):发现指数巨大,使用欧拉定理,将问题转化为求指数 \(\pmod{P-1}\)。
- 第二层(分解):发现新模数 \(P-1\) 是合数,使用中国剩余定理,将问题分解为求指数 \(\pmod{\text{各个质因子}}\)。
- 第三层(求解):在每个子问题中,需要计算组合数模小质数,使用卢卡斯定理高效求解。
- 返回与合并:用卢卡斯定理求出各个余数后,用CRT合并,得到最终的指数 \(S\)。最后,通过快速幂计算 \(G^S \pmod{P}\) 得到答案。
这真是一趟精彩的数论之旅!通过这道题,我们复习并实践了数论中几个最核心的定理,将一个看似无从下手的问题,分解成了若干个我们能够解决的模板子问题。
附上与题解思路一致的代码,以供参考。
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
#define Mod 999911659 // 最终的模数 (质数)
#define mod 999911658 // 指数的模数 (合数)
#define maxn 40005 // 预处理阶乘的最大值,比所有质因子都大即可
typedef long long ll;
ll n, g;
ll d[maxn], tot; // 存储n的约数
ll p[10], cnt; // 存储mod的质因子
// 快速幂,用于计算 a^k mod p
inline ll qpow(ll a, ll k, ll p) {
ll res = 1;
a %= p; // 别忘了取模
while (k) {
if (k & 1) res = (res * a) % p;
a = (a * a) % p;
k >>= 1;
}
return res;
}
// 预处理阶乘和逆元,用于计算小范围组合数
ll fac[maxn], inv[maxn];
inline void init(ll p) {
fac[0] = 1;
for (int i = 1; i < p; i++) {
fac[i] = (fac[i - 1] * i) % p;
}
inv[p - 1] = qpow(fac[p - 1], p - 2, p); // 费马小定理求逆元
for (int i = p - 2; i >= 0; i--) {
inv[i] = (inv[i + 1] * (i + 1)) % p;
}
}
// 计算 C(n, m) mod p,其中 n, m < p
inline ll C(ll n, ll m, ll p) {
if (m > n) return 0;
return fac[n] * inv[m] % p * inv[n - m] % p;
}
// 卢卡斯定理,计算 C(n, m) mod p
inline ll Lucas(ll n, ll m, ll p) {
if (m == 0) return 1;
return (Lucas(n / p, m / p, p) * C(n % p, m % p, p)) % p;
}
// a[x] 存储 sum(C(n,d)) mod p[x] 的结果
ll a[10];
// 计算 sum(C(n,d)) mod p[x]
inline void calc(int x) {
ll prime = p[x];
init(prime); // 针对当前质数预处理
for (int i = 1; i <= tot; i++) {
a[x] = (a[x] + Lucas(n, d[i], prime)) % prime;
}
}
// 中国剩余定理 (CRT)
inline ll CRT() {
ll ans = 0;
for (int i = 1; i <= cnt; i++) {
ll M = mod / p[i];
ll t = qpow(M, p[i] - 2, p[i]); // M 在 mod p[i] 下的逆元
// 累加 a[i] * M * t
ans = (ans + a[i] * t % mod * M % mod) % mod;
}
return (ans + mod) % mod;
}
int main() {
scanf("%lld%lld", &n, &g);
// 特判:如果 g 是 Mod 的倍数,结果为 0
if (g % Mod == 0) {
printf("0\n");
return 0;
}
// 1. 分解指数的模数 999911658
ll t = mod;
for (ll i = 2; i * i <= t; i++) {
if (t % i == 0) {
p[++cnt] = i;
while (t % i == 0) t /= i;
}
}
if (t > 1) p[++cnt] = t;
// 2. 找出 n 的所有约数
for (ll i = 1; i * i <= n; i++) {
if (n % i == 0) {
d[++tot] = i;
if (i * i != n) d[++tot] = n / i;
}
}
// 3. 对每个质因子,用卢卡斯定理计算余数
for (int i = 1; i <= cnt; i++) {
calc(i);
}
// 4. 用CRT合并结果,得到最终的指数
ll sum_C = CRT();
// 5. 快速幂求最终答案
printf("%lld\n", qpow(g, sum_C, Mod));
return 0;
}