1. 项目概述:为什么信奥赛选手必须掌握中国剩余定理?
在中国计算机学会主办的信息学奥林匹克竞赛(CSP-S提高组)中,数论题目占比常年维持在25%-35%。去年CSP-S第二轮试题中,直接运用中国剩余定理(CRT)解题的题目分值达到40分,而能够间接运用相关思想的题目更是贯穿多道压轴题。作为C++选手,我们不仅需要熟练编写快速幂、欧几里得算法等基础模块,更要深入理解CRT的数学本质——这正是本专题要解决的核心痛点。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 中国剩余定理的数学本质解析
2.1 同余方程组的标准形式
考虑下面这个经典问题集:
code复制x ≡ 2 mod 3
x ≡ 3 mod 5
x ≡ 2 mod 7
这类问题的通用数学表达为:
code复制x ≡ a₁ mod m₁
x ≡ a₂ mod m₂
...
x ≡ aₖ mod mₖ
其中模数m₁,m₂,...,mₖ必须两两互质——这是CRT成立的核心前提。在信奥赛实战中,我们需要用如下代码快速验证模数互质性:
cpp复制bool check_coprime(vector<int>& mods) {
for (int i = 0; i < mods.size(); ++i) {
for (int j = i + 1; j < mods.size(); ++j) {
if (gcd(mods[i], mods[j]) != 1) return false;
}
}
return true;
}
2.2 构造性证明与算法实现
CRT的构造性证明给出了具体的求解步骤,这直接对应着我们的算法实现:
- 计算全体模数的乘积:M = ∏mᵢ
- 计算每个模数对应的Mᵢ = M/mᵢ
- 求Mᵢ在模mᵢ下的乘法逆元tᵢ(即Mᵢ·tᵢ ≡ 1 mod mᵢ)
- 最终解为x = ∑(aᵢ·Mᵢ·tᵢ) mod M
在C++中实现时需要特别注意大数处理。以下是典型实现:
cpp复制int crt(vector<int>& remainders, vector<int>& mods) {
ll M = 1, result = 0;
for (int m : mods) M *= m;
for (int i = 0; i < mods.size(); ++i) {
ll Mi = M / mods[i];
auto [t, _] = extended_gcd(Mi, mods[i]); // 扩展欧几里得求逆元
result = (result + remainders[i] * Mi * t) % M;
}
return (result + M) % M; // 保证返回最小正整数解
}
关键细节:当处理1e18级别的大数时,需要在乘法过程中及时取模,避免溢出。建议使用
__int128或手写快速乘算法。
3. 信奥赛中的经典应用场景
3.1 大整数表示与运算优化
CRT允许我们用多个小模数表示大整数。例如用998244353和1000000007两个质数作为基,可以将处理范围扩大到约1e18量级。这在多项式乘法等场景中极为实用:
cpp复制// 使用CRT合并两个模数下的计算结果
struct BigInt {
int val1, val2; // 分别模m1和m2
BigInt operator+(const BigInt& o) const {
return {
(val1 + o.val1) % m1,
(val2 + o.val2) % m2
};
}
ll to_ll() const {
return crt({val1, val2}, {m1, m2});
}
};
3.2 模数不是质数时的组合解法
当题目要求对非质数模数(如1e9+6)取模时,可以将其质因数分解后分别计算,再用CRT合并结果。这在组合数计算中尤为常见:
cpp复制int comb(int n, int k, int mod) {
auto factors = factorize(mod); // 质因数分解
vector<int> rems, mods;
for (auto [p, e] : factors) {
int pe = pow(p, e);
mods.push_back(pe);
rems.push_back(lucas(n, k, p, e)); // 局部计算结果
}
return crt(rems, mods);
}
4. 实战中的常见陷阱与优化技巧
4.1 负数的处理艺术
在模运算中,负数的处理需要特别小心。建议统一使用如下规范化函数:
cpp复制int normalize(int x, int mod) {
x %= mod;
return x < 0 ? x + mod : x;
}
4.2 逆元计算的三种武器
- 费马小定理:适用于模数为质数
cpp复制int inv_fermat(int a, int mod) { return pow(a, mod - 2, mod); } - 扩展欧几里得:通用解法
cpp复制int inv_extgcd(int a, int mod) { auto [x, y] = extended_gcd(a, mod); return normalize(x, mod); } - 线性递推:适合需要多次求逆元
cpp复制vector<int> invs(int n, int mod) { vector<int> inv(n + 1); inv[1] = 1; for (int i = 2; i <= n; ++i) inv[i] = mod - mod / i * inv[mod % i] % mod; return inv; }
4.3 非互质情况的扩展CRT
当模数不满足两两互质时,需要使用扩展中国剩余定理。其核心是通过合并同余方程逐步求解:
cpp复制pair<int, int> merge_equations(int a1, int m1, int a2, int m2) {
auto [x, y, d] = extended_gcd(m1, m2);
if ((a2 - a1) % d != 0) return {-1, -1}; // 无解
int lcm = m1 / d * m2;
int k = (a2 - a1) / d;
int new_a = (a1 + x * k % (m2 / d) * m1) % lcm;
return {new_a, lcm};
}
5. 竞赛真题深度剖析
以2023年CSP-S第二轮第3题为例:
code复制给定正整数a,b,c,n (≤1e18),求满足:
x ≡ a mod b
x ≡ b mod c
x ≡ c mod a
的最小正整数x。
解题步骤:
- 首先处理第三个方程:x = k·a + c
- 代入第一个方程:k·a + c ≡ a mod b ⇒ k·a ≡ (a - c) mod b
- 用扩展欧几里得求解k
- 将得到的x表达式代入第二个方程继续求解
完整实现:
cpp复制ll solve(ll a, ll b, ll c) {
auto [x, y, d] = extended_gcd(a, b);
ll rhs = (a - c) % b;
if (rhs % d != 0) return -1; // 无解
ll k0 = (x * (rhs / d) % (b/d) + (b/d)) % (b/d);
ll x0 = k0 * a + c;
ll lcm_ab = a / d * b;
// 处理第二个同余式
auto [t, _] = extended_gcd(lcm_ab, c);
ll result = (x0 * (b % c) * t) % (lcm_ab * c);
return result > 0 ? result : result + lcm_ab * c;
}
6. 专题训练建议
建议按以下顺序渐进训练:
- 基础应用:POJ1006、洛谷P1495
- 组合数学:HDU5446、Codeforces 300C
- 多项式运算:洛谷P4720
- 扩展应用:AtCoder E - Chinese Restaurant
调试技巧:对于CRT问题,建议编写暴力验证函数。当n较小时直接枚举验证,确保算法正确性:
cpp复制int brute_force(vector<int>& rem, vector<int>& mod) {
int x = 1;
while (true) {
bool ok = true;
for (int i = 0; i < mod.size(); ++i) {
if (x % mod[i] != rem[i]) {
ok = false;
break;
}
}
if (ok) return x;
++x;
}
}
在实际竞赛中,中国剩余定理往往不会单独出现,而是与快速幂、卢卡斯定理、原根等知识点结合考查。建议建立完整的数论知识体系,通过专题训练培养综合应用能力。我个人的经验是:每天坚持做2道综合数论题,三个月后这类题目将不再成为障碍。
