信奥赛C++提高组选手在算法竞赛中,数论知识往往是区分选手水平的关键分水岭。这套专题课聚焦竞赛中最实用的数论工具链,从同余概念一直延伸到分数模运算这个高阶技巧。我在带竞赛队伍的十年中发现,约85%的选手在初次接触NOIP/CSP-S数论题时,最大的障碍不是代码实现,而是对数学原理的理解不透彻。
这个专题设计的独特之处在于,它按照竞赛命题的思维路径组织知识点。比如2021年CSP-S提高组的那道关于密码破解的压轴题,解题核心就是裴蜀定理和乘法逆元的组合应用。课程会先建立完整的理论框架,然后用ACM-ICPC区域赛真题作为案例,最后给出可复用的C++模板代码。
同余关系"a ≡ b (mod m)"的本质是m整除(a-b)。这个看似简单的定义在竞赛中衍生出多种变化:
重要性质证明示例(以乘法性质为例):
若a ≡ b (mod m), c ≡ d (mod m),则ac ≡ bd (mod m)。证明:
a = km + b, c = tm + d ⇒ ac = (km+b)(tm+d) = ktm² + (kd+bt)m + bd
显然m整除前两项,故ac ≡ bd (mod m)
cpp复制int mod(int a, int m) {
return (a % m + m) % m;
}
cpp复制int big_mod(string s, int m) {
int res = 0;
for(char c : s)
res = (res * 10 + (c-'0')) % m;
return res;
}
构造性证明:
设d = gcd(a,b),通过欧几里得算法回溯可得整数x,y使ax+by=d。这个过程直接给出了扩展欧几里得算法的实现思路。
集合极小性证明:
考虑集合S = {ax+by | x,y∈Z}中的最小正整数d,证明d是a,b的最大公约数。这个方法揭示了系数的存在性但不直接给出构造。
问题:给定a,b,c,判断是否存在整数x,y使ax+by=c
解法模板:
cpp复制bool check(int a, int b, int c) {
int d = __gcd(a,b);
return c % d == 0;
}
变式:若存在,求|x|+|y|最小的解。这需要先用扩展欧几里得求出特解,然后通过参数调整寻找最优解。
递归版本(更直观):
cpp复制int exgcd(int a, int b, int &x, int &y) {
if(!b) { x=1; y=0; return a; }
int d = exgcd(b, a%b, y, x);
y -= a/b * x;
return d;
}
迭代版本(性能更优):
cpp复制int exgcd(int a, int b, int &x, int &y) {
x = 1, y = 0;
int x1 = 0, y1 = 1;
while(b) {
int q = a / b;
tie(x, x1) = make_tuple(x1, x - q * x1);
tie(y, y1) = make_tuple(y1, y - q * y1);
tie(a, b) = make_tuple(b, a - q * b);
}
return a;
}
若x₀,y₀是ax+by=gcd(a,b)的特解,则通解为:
x = x₀ + k·(b/d)
y = y₀ - k·(a/d)
其中d=gcd(a,b),k∈Z
这个结构在求解线性同余方程时至关重要,例如:
求方程7x ≡ 3 (mod 12)的解:
这是最通用的方法,适用于任意模数m:
cpp复制int inv(int a, int m) {
int x, y;
int d = exgcd(a, m, x, y);
return d == 1 ? (x + m) % m : -1;
}
时间复杂度O(log min(a,m)),适合单次查询。
当m为质数时,a^(m-2)即为逆元:
cpp复制int qpow(int a, int b, int m);
int inv(int a, int m) {
return qpow(a, m-2, m);
}
配合快速幂,时间复杂度O(log m)。
预处理1~n的逆元,O(n)时间复杂度:
cpp复制vector<int> inv(n+1);
inv[1] = 1;
for(int i=2; i<=n; ++i)
inv[i] = (m - m/i) * inv[m%i] % m;
这个方法的推导基于m = k·i + r,其中k=⌊m/i⌋,r=m mod i。
分数a/b mod m的计算公式:
a/b ≡ a·b⁻¹ (mod m)
其中b⁻¹是b模m的逆元。
验证示例:
计算3/7 mod 5:
7⁻¹ ≡ 3 (mod 5),因为7×3=21≡1 (mod 5)
所以3/7 ≡ 3×3=9≡4 (mod 5)
连分数处理:
计算(a/(b/c)) mod m时,应该转化为a·c·b⁻¹ mod m,注意不能直接嵌套计算。
多项式分式处理:
在生成函数等问题中,经常需要计算形如P(x)/Q(x) mod m的情况,这时需要对分母的每个非零项求逆元。
题目要求解形如a₁x ≡ b₁ (mod m₁), ..., aₙx ≡ bₙ (mod mₙ)的方程组。解题步骤:
关键代码段:
cpp复制bool crt(int &a1, int &m1, int a2, int m2) {
int p, q;
int d = exgcd(m1, m2, p, q);
if((a2-a1)%d) return false;
int lcm = m1/d*m2;
a1 = (a1 + (a2-a1)/d*p%(m2/d)*m1) % lcm;
m1 = lcm;
return true;
}
需要动态维护路径上的乘积逆元。解决方案:
cpp复制int qpow(int a, int b, int m) {
int res = 1;
for(; b; b>>=1, a=(long long)a*a%m)
if(b&1) res=(long long)res*a%m;
return res;
}
对于1e6以内的逆元计算,可以使用静态数组代替vector:
cpp复制const int N = 1e6;
int inv[N+1];
void init() {
inv[1] = 1;
for(int i=2; i<=N; ++i)
inv[i] = (MOD - MOD/i) * 1LL * inv[MOD%i] % MOD;
}
当需要频繁查询不同模数下的逆元时,可以预处理质数表,然后根据模数的质因数分解快速计算。
忘记检查逆元存在性:
在计算a⁻¹ mod m前,必须先确认gcd(a,m)=1
整数溢出问题:
在中间步骤使用long long,特别是在乘法运算前强制转换
负数的模运算:
确保最终结果在[0,m-1]范围内
递归深度过大:
对于大数情况,优先使用迭代版exgcd
调试技巧:
这些知识在省选及以上级别的竞赛中经常出现,建议在掌握本专题内容后逐步学习。我带的队伍通常会安排每周一个高阶数论专题的研讨,配合3-5道对应难度的编程习题。