
1. 从“物不知数”到现代密码学一个古老定理的现代生命力“今有物不知其数三三数之剩二五五数之剩三七七数之剩二问物几何” 这道出自《孙子算经》的经典题目相信很多人在学生时代都见过。它描述了一个寻找满足多个同余条件的整数的问题。而解决这类问题的通用方法就是我们今天要深入探讨的中国剩余定理。别被这个名字里的“定理”二字吓到它本质上是一个极其强大且优雅的构造性算法不仅在数学竞赛中频频现身更是现代密码学、计算机科学、乃至工程计算中不可或缺的基石。比如RSA加密算法的加速运算、多精度大数计算、乃至分布式系统中的一致性校验背后都有它的身影。简单来说中国剩余定理解决的是给定一组两两互质的模数以及每个模数对应的余数如何高效地找到一个或所有满足所有同余条件的解。而扩展中国剩余定理则是当模数不再两两互质时对原定理的推广它通过求解线性同余方程组将适用范围扩大到了更一般的情形。理解这两个定理不仅仅是掌握两个数学公式更是掌握一种“分而治之”的系统性思维——将复杂的大问题分解为若干个简单的、独立的小问题来解决最后再将结果精巧地组合起来。这篇文章我将从一个一线开发者和算法爱好者的角度带你彻底吃透中国剩余定理及其扩展形式。我们会从最朴素的枚举法开始一步步推导出定理的构造性证明并亲手实现其算法。然后我们会直面模数不互质时的挑战深入扩展中国剩余定理的推导与实现。最后我会分享几个在算法竞赛和实际工程中应用它们的经典场景与避坑经验。无论你是正在备战信息学竞赛的学生还是对底层算法原理感兴趣的开发者相信这篇融合了数学原理与代码实战的深度解析都能让你有所收获。2. 中国剩余定理互质模数下的优雅构造2.1 问题形式化与朴素解法首先让我们把《孙子算经》的问题用现代数学语言重新表述。中国剩余定理要解决的是如下形式的同余方程组x ≡ a1 (mod m1) x ≡ a2 (mod m2) ... x ≡ an (mod mn)其中m1, m2, ..., mn是两两互质的正整数即任意两个数的最大公约数 gcd(mi, mj) 1, i ≠ ja1, a2, ..., an是给定的整数。我们的目标是找到一个整数x使得它同时满足所有n个同余方程。显然如果存在一个解x0那么x0加上所有模数的最小公倍数M m1 * m2 * ... * mn的任意整数倍也都是解。因此我们通常寻找在0到M-1范围内的唯一解。最笨的办法当然是枚举。对于上面的“物不知数”问题模数乘积M 3*5*7 105我们可以在0到104之间逐个尝试。但一旦模数变大或者数量增多M会急剧膨胀枚举法就完全不可行了。我们需要一个系统性的构造方法。2.2 定理的构造性证明与核心思想中国剩余定理的精妙之处在于它提供了一个直接的构造公式。令M m1 * m2 * ... * mn并令Mi M / mi即Mi是除了mi之外所有模数的乘积。由于所有mi两两互质所以Mi和mi也互质gcd(Mi, mi) 1。根据数论中的裴蜀定理对于互质的两个数存在整数ti和si使得Mi * ti mi * si 1。这个等式模mi后因为mi * si项被模掉了我们就得到了Mi * ti ≡ 1 (mod mi)。换句话说ti就是Mi在模mi意义下的乘法逆元。我们记inv_i为Mi模mi的逆元即Mi * inv_i ≡ 1 (mod mi)。那么中国剩余定理断言方程组的解x可以构造为x (a1 * M1 * inv1 a2 * M2 * inv2 ... an * Mn * invn) mod M为什么这个构造是有效的我们验证一下它对第一个方程x ≡ a1 (mod m1)是否成立。 观察求和式中的每一项对于i 1的项a1 * M1 * inv1。因为M1 * inv1 ≡ 1 (mod m1)所以这一项模m1等于a1。对于i ≠ 1的项例如a2 * M2 * inv2。注意M2 M / m2其中包含了因子m1因为模数两两互质。因此M2是m1的整数倍所以a2 * M2 * inv2 ≡ 0 (mod m1)。 将所有项相加后模m1非第一项全为0只剩下第一项贡献的a1。因此x ≡ a1 (mod m1)。对其他方程同理可证。这个构造过程完美体现了“分治”思想我们将寻找全局解x的任务分解为寻找每个局部方程贡献的“分量”。每个分量ai * Mi * inv_i被设计成只对第i个方程有效模mi余ai而对其他方程无效模其他mj余0。最后将这些分量在模M的意义下加起来就得到了全局解。2.3 算法实现与代码详解理解了构造原理实现起来就非常直观了。算法的核心步骤是计算所有模数的乘积M。对于每个i计算Mi M / mi。计算Mi在模mi意义下的逆元inv_i。根据公式构造解x。将x调整到[0, M-1]范围内。这里的关键是第3步如何求逆元因为mi不一定是质数我们不能直接用费马小定理。我们需要使用扩展欧几里得算法来求解Mi * inv_i mi * k 1中的inv_i。扩展欧几里得算法是数论的基础工具它不仅能求出最大公约数还能求出裴蜀等式中的系数。下面是用Python实现的中国剩余定理函数def exgcd(a, b): 扩展欧几里得算法返回 (gcd, x, y) 满足 a*x b*y gcd(a, b) if b 0: return a, 1, 0 gcd, x1, y1 exgcd(b, a % b) x y1 y x1 - (a // b) * y1 return gcd, x, y def mod_inv(a, m): 求 a 在模 m 意义下的逆元假设 gcd(a, m) 1 gcd, x, _ exgcd(a, m) if gcd ! 1: raise ValueError(逆元不存在a 和 m 不互质) return x % m # 确保逆元是正数 def crt(a_list, m_list): 中国剩余定理求解同余方程组。 参数: a_list: 余数列表 [a1, a2, ..., an] m_list: 两两互质的模数列表 [m1, m2, ..., mn] 返回: 满足方程组的最小非负整数解 x (在 [0, M-1] 内)以及模数乘积 M。 如果无解理论上在互质情况下必有解返回 (None, None)。 n len(a_list) # 1. 计算所有模数的乘积 M M 1 for m in m_list: M * m x 0 for i in range(n): ai a_list[i] mi m_list[i] # 2. 计算 Mi Mi M // mi # 3. 计算 Mi 模 mi 的逆元 try: inv_i mod_inv(Mi, mi) except ValueError: # 理论上不会发生因为输入保证了 mi 两两互质所以 Mi 和 mi 也互质 return None, None # 4. 累加构造解 x ai * Mi * inv_i # 5. 取模得到最小非负解 x % M return x, M让我们用“物不知数”问题测试一下a_list [2, 3, 2],m_list [3, 5, 7]。 调用crt([2,3,2], [3,5,7])计算过程如下M 3*5*7 105对于 i0:M0105/335, 求35 mod 3的逆元。35 ≡ 2 (mod 3)2模3的逆元是2因为2*24≡1 mod 3。贡献项2 * 35 * 2 140。对于 i1:M1105/521,21 ≡ 1 (mod 5)逆元是1。贡献项3 * 21 * 1 63。对于 i2:M2105/715,15 ≡ 1 (mod 7)逆元是1。贡献项2 * 15 * 1 30。总和1406330233。233 mod 105 23。 所以最小正整数解是23。代入验证23除以3余2除以5余3除以7余2完全正确。注意在实际编码中尤其是处理大数时累加ai * Mi * inv_i可能会溢出。在Python中整数无上限所以没问题但在C/Java等语言中需要在累加过程中及时取模M即x (x ai * Mi % M * inv_i) % M。这是一个常见的性能与安全性优化点。3. 当模数不互质时扩展中国剩余定理的挑战与征服中国剩余定理要求模数两两互质这是一个很强的条件。在实际问题中我们遇到的模数很可能不是互质的。例如方程组x ≡ 2 (mod 4) x ≡ 3 (mod 6)这里m14,m26gcd(4,6)2不互质。如果我们强行套用CRT公式计算M16, 需要找6 mod 4的逆元即2 mod 4的逆元。但2和4不互质逆元不存在公式失效。那么模数不互质时方程还有解吗如何求解这就是扩展中国剩余定理要解决的问题。3.1 从两个方程开始合并的思想EXCRT的核心思想是逐步合并。我们不再试图一次性构造出整个解而是每次只合并两个方程将其化简为一个等价的方程直到合并所有方程为一个方程x ≡ a (mod m)此时的a就是最终解模m意义下。考虑两个方程x ≡ a1 (mod m1) x ≡ a2 (mod m2)我们可以将x写成x a1 k1 * m1的形式其中k1是某个整数。将其代入第二个方程a1 k1 * m1 ≡ a2 (mod m2)这等价于一个关于k1的线性同余方程m1 * k1 ≡ a2 - a1 (mod m2)令d gcd(m1, m2)。我们知道线性同余方程m1 * k1 ≡ c (mod m2)有解当且仅当d能整除c这里c a2 - a1。这是解存在的充要条件。如果d不能整除(a2 - a1)那么这两个方程本身就矛盾整个方程组无解。例如x ≡ 1 (mod 2)和x ≡ 0 (mod 2)显然无解。如果d能整除(a2 - a1)那么方程有解。我们可以将方程两边以及模数同时除以d得到一个等价的、系数与模数互质的方程(m1/d) * k1 ≡ (a2 - a1)/d (mod m2/d)现在gcd(m1/d, m2/d) 1所以(m1/d)在模(m2/d)下有逆元。设其逆元为inv则我们可以解出k1k1 ≡ [(a2 - a1)/d * inv] (mod m2/d)设这个特解为k1 k0那么k1的通解形式为k1 k0 t * (m2/d)其中t是任意整数。将k1的通解代回x a1 k1 * m1x a1 (k0 t * (m2/d)) * m1 a1 k0*m1 t * (m1*m2/d)我们注意到m1*m2/d正是m1和m2的最小公倍数lcm(m1, m2)。因此x可以写成x ≡ a1 k0*m1 (mod lcm(m1, m2))这样我们就将原来的两个方程合并为了一个新的方程x ≡ A (mod M)其中A a1 k0*m1M lcm(m1, m2)。这个新方程的解集完全等价于原来两个方程联立的解集。3.2 算法流程与逐步合并实现基于上述两个方程的合并方法EXCRT的算法可以描述为初始化当前解为第一个方程x a1,m m1。从第二个方程开始依次与当前方程(x, m)进行合并。对于第i个方程(ai, mi) a. 设d gcd(m, mi)c ai - x。 b. 如果d不能整除c则整个方程组无解。 c. 否则令k (c/d) * inv(m/d, mi/d) mod (mi/d)其中inv(a, b)是a模b的逆元。 d. 更新x x k * m。 e. 更新m lcm(m, mi) m * mi / d。 f. 将x对新的m取模得到最小非负特解。合并完所有方程后最终的x就是方程组在模m所有模数的最小公倍数意义下的最小非负解。下面是Python实现def excrt(a_list, m_list): 扩展中国剩余定理求解同余方程组不要求模数互质。 参数: a_list: 余数列表 [a1, a2, ..., an] m_list: 模数列表 [m1, m2, ..., mn] (可以不互质) 返回: 满足方程组的最小非负整数解 x (在 [0, lcm(m_list)-1] 内)以及所有模数的最小公倍数 lcm。 如果无解返回 (None, None)。 n len(a_list) # 初始化第一个方程 x a_list[0] m m_list[0] for i in range(1, n): ai a_list[i] mi m_list[i] # 将当前解表示为 x t*m代入新方程 ai ≡ x t*m (mod mi) # 得到关于 t 的方程 m*t ≡ ai - x (mod mi) c (ai - x) % mi # 确保c是非负数便于计算 # 求解线性同余方程 m*t ≡ c (mod mi) d, t0, _ exgcd(m, mi) # d gcd(m, mi), t0 是 m/d 在模 mi/d 意义下的一个系数 if c % d ! 0: # 无解 return None, None # 化简方程 mi_d mi // d # t0 是 m/d 模 mi/d 的逆元乘以 (m/d) 的系数我们需要的是 (m/d) 的逆元。 # 注意 exgcd 返回的 t0 满足 m*t0 mi*_ d。 # 所以 (m/d)*t0 (mi/d)*_ 1。因此 t0 就是 (m/d) 模 (mi/d) 的一个逆元。 # 但需要调整到正数范围。 t0 % mi_d # 计算特解 t t (c // d * t0) % mi_d # 更新解 x 和模数 m x x t * m m m * mi_d # m lcm(m, mi) m * (mi // d) x % m # 取最小非负解 return x, m让我们测试一个例子求解x ≡ 2 (mod 4), x ≡ 3 (mod 6)。初始化x2, m4。合并第二个方程(3, 6)c (3-2) % 6 1。d gcd(4,6)2。c % d 1 % 2 1不为0所以无解。这与我们的直觉一致一个数模4余2说明它是偶数模6余3说明它是奇数。矛盾。 再测试一个有解的例子x ≡ 2 (mod 4), x ≡ 4 (mod 6)。初始化x2, m4。合并(4, 6)c (4-2) % 6 2。d gcd(4,6)2。c % d 0有解。mi_d 6/23。解4*t ≡ 2 (mod 6)。化简为2*t ≡ 1 (mod 3)。2模3的逆元是2所以t ≡ 2 (mod 3)。取t2。更新x 2 2*4 10。m lcm(4,6)12。x % 12 x10。最终解x ≡ 10 (mod 12)。验证10 mod 4 210 mod 6 4。正确。实操心得在实现EXCRT时最容易出错的地方是逆元的计算和模运算。exgcd返回的系数t0可能是负数必须将其调整到[0, mi_d-1]范围内t0 % mi_d。另外计算c ai - x时直接相减可能得到负数最好先取模mi确保非负避免后续求余和判断整除时出现符号问题。这是很多人在竞赛中丢分的坑。4. 算法细节深潜与边界情况处理4.1 大数运算与溢出防范在实际应用尤其是密码学或处理大规模数据时模数和中间结果可能非常大例如1024位以上的大整数。虽然Python原生支持大整数但在C、Java等语言中我们必须警惕溢出问题。在CRT和EXCRT的构造过程中涉及多次乘法如ai * Mi * inv_i或x k * m。Mi是除mi外所有模数的乘积在模数较多或较大时Mi本身就可能超出64位整数范围。即使最终结果会对M取模但中间计算过程的溢出会导致错误。解决方案是及时取模。以CRT为例正确的累加方式应该是x 0 M prod(m_list) for i in range(n): Mi M // m_list[i] inv_i mod_inv(Mi % m_list[i], m_list[i]) # 求逆元时也可以先取模因为 (a mod m) 的逆元等于 a 的逆元 term (a_list[i] * Mi) % M term (term * inv_i) % M x (x term) % M每一步乘法后都立即对M取模可以保证中间结果始终在[0, M-1]范围内避免溢出。对于EXCRT中的x x t * m更新也应写为x (x (t % (m_new // m_old)) * m_old) % m_new的形式其中m_new是新的lcm。4.2 解的唯一性与通解形式无论是CRT还是EXCRT我们找到的都是一个最小非负特解x0。那么方程组的全部解是什么对于CRT模数两两互质所有解构成一个模M的同余类。即通解为x x0 k * Mk为任意整数。在[0, M-1]范围内解是唯一的。对于EXCRT模数不一定互质所有解构成一个模L的同余类其中L是所有模数的最小公倍数lcm(m1, m2, ..., mn)。即通解为x x0 k * Lk为任意整数。在[0, L-1]范围内解也是唯一的。这里有一个关键点解存在的充要条件。对于CRT由于模数互质解总是存在。对于EXCRT解存在的充要条件是对于任意两个方程i和j都有ai ≡ aj (mod gcd(mi, mj))。我们的合并算法在每一步都检查了等价的d | (ai - x)条件本质上就是在动态验证这个一致性条件。如果合并过程中某一步失败则说明方程组无解。4.3 负余数与模运算的处理在问题描述中余数ai通常是给定的非负整数0 ai mi。但有时我们也会遇到负余数的情况例如x ≡ -2 (mod 5)。这在数学上是完全等价的因为-2 mod 5 3 mod 5。我们的算法应该能处理这种情况。处理原则是在计算前将所有余数规范到其对应模数的非负剩余系中。即对于每个方程x ≡ ai (mod mi)我们计算ai ai % mi。如果ai是负数%操作在Python中会返回一个正余数例如-2 % 5 3。在C/Java中%可能返回负数需要手动调整ai (ai % mi mi) % mi。在EXCRT的合并步骤中计算c ai - x时我们也应该对mi取模即c (ai - x) % mi以确保c是非负的便于后续判断d | c和计算逆元。这是一个良好的编程习惯能避免很多因负数取模行为不同而导致的隐蔽错误。5. 从理论到实战应用场景与经典问题剖析理解了原理和实现我们来看看它们能解决哪些实际问题。我挑选了几个在算法竞赛和工程中非常典型的应用场景。5.1 场景一大整数的多模数表示与快速计算这是CRT在计算机代数系统中的一个经典应用。假设我们需要对非常大的整数比如上千位进行频繁的加、减、乘运算。直接使用高精度算法效率较低。我们可以利用CRT来加速表示选取一组两两互质且乘积足够大的模数{m1, m2, ..., mn}。对于一个大整数X我们不直接存储它而是存储它在每个模数下的余数(x1, x2, ..., xn)其中xi X mod mi。这被称为“剩余数系统”。运算要对两个大整数A和B进行运算如乘法我们只需对它们对应的余数序列进行分量运算ci (ai * bi) mod mi。因为模运算下(A*B) mod mi ((A mod mi) * (B mod mi)) mod mi。所有运算都在较小的模数mi下进行速度极快且可以并行计算。还原当需要得到最终结果C的数值时再使用CRT将余数序列(c1, c2, ..., cn)还原为C mod MM为所有模数乘积。只要最终结果C的绝对值小于M/2通常通过选择足够大的M来保证我们就能唯一确定C。这种方法将大规模整数运算分解为多个独立的小规模模运算在硬件并行或分布式计算中潜力巨大。RSA解密运算中利用CRT称为RSA-CRT可以将解密速度提升近4倍其原理就是先对模数np*q分解分别在模p和模q下计算再用CRT合成结果。5.2 场景二线性同余方程组的求解这是EXCRT最直接的应用。很多问题最终可以转化为求解形如x ≡ ai (mod mi)的方程组。例如问题有一个数除以3余2除以5余1除以6余4求满足条件的最小正整数。 这就是一个标准的同余方程组x ≡ 2 (mod 3), x ≡ 1 (mod 5), x ≡ 4 (mod 6)。模数3,5,6不两两互质gcd(3,6)3必须使用EXCRT。 调用excrt([2,1,4], [3,5,6])合并前两个(2,3)和(1,5)dgcd(3,5)1有解得到x ≡ 11 (mod 15)。再合并(4,6)当前x11, m15。c(4-11)%65dgcd(15,6)3。c % d 5 % 3 2 ≠ 0。无解。 这意味着不存在一个整数同时满足这三个条件。通过EXCRT我们高效地得出了无解的结论。5.3 场景三周期性问题与时间推算这类问题在竞赛和面试中很常见通常描述为某个事件以多个周期循环发生求下一次同时满足多个条件的时间点。经典例题三个机器人从起点同时出发。机器人A每3分钟回起点一次B每5分钟回一次C每7分钟回一次。问至少多少分钟后它们第一次同时回到起点并相遇 设时间为t分钟。那么t必须是357的倍数。即t ≡ 0 (mod 3), t ≡ 0 (mod 5), t ≡ 0 (mod 7)。余数全是0这就是CRT的特殊情况。解为t ≡ 0 (mod lcm(3,5,7)105)。所以最少105分钟。这里直接求最小公倍数即可但模型本质是CRT。更复杂的变体A从起点出发B比A晚1分钟出发C比A晚2分钟出发它们的周期仍是3,5,7分钟。求第一次同时回到起点的时间假设速度恒定同时到达起点即相遇。 那么条件变为t ≡ 0 (mod 3)t ≡ 1 (mod 5)因为B需要比A多花1分钟走完自己的周期这里需要仔细建模。实际上如果B的周期是5分钟且晚1分钟出发那么B回到起点的时间满足t ≡ 4 (mod 5)因为第0分钟时B在起点后第1分钟的位置它需要再走4分钟才能第一次回起点。同理Ct ≡ 5 (mod 7)或t ≡ 5 mod 7需要计算。这就构成了一个标准的同余方程组可以用CRT求解因为3,5,7互质。5.4 踩坑实录EXCRT实现中的常见错误在我最初实现EXCRT时踩过几个典型的坑这里分享出来帮你避雷逆元求解的误解在合并方程m*t ≡ c (mod mi)时我们化简得到(m/d)*t ≡ c/d (mod mi/d)。很多人会直接去计算(m/d)模mi/d的逆元inv然后t (c/d) * inv mod (mi/d)。这没错。但在代码中我们通过exgcd(m, mi, d, t0, _)得到了t0。注意这个t0满足m*t0 mi*_ d。两边除以d得(m/d)*t0 (mi/d)*_ 1。这意味着t0本身就是(m/d)模mi/d的一个逆元所以不需要再调用一次求逆函数直接用t0即可但务必记得t0可能为负需要取模调整。更新模数时的顺序合并后新的模数应该是lcm(m, mi) m * (mi / d)。计算顺序很重要。必须先计算lcm再用新的lcm去取模更新x。错误的顺序如x (x t * m) % (m * mi // d)在逻辑上等价但如果在更新x之前先计算m m * mi // d那么公式里的m就变成了新的值导致计算错误。正确的做法是lcm m // d * mi # 先计算lcm注意先除后乘防溢出 x x t * m # 用旧的m更新x x x % lcm # 用新的lcm取模 m lcm # 最后更新m无解判断的遗漏在合并过程中必须检查d | c。如果忘记检查当方程组无解时程序会继续运行并给出一个错误的结果。这是一个逻辑完整性检查绝不能省略。数据类型的溢出如前所述即使在Python中养成及时取模的习惯也是好的实践。在C中可以使用__int128或手动实现快速乘龟速乘来防止中间过程溢出。例如计算(a * b) % mod时如果a和b都接近1e18直接乘会溢出long long需要用__int128或(a % mod) * (b % mod) % mod结合快速乘函数。理解了中国剩余定理和扩展中国剩余定理你就掌握了一把解决离散模数系统问题的万能钥匙。从古老的数学谜题到现代的加密通信其核心的“分解-求解-合并”思想贯穿始终。实现它们的关键在于对扩展欧几里得算法和模运算的深刻理解与熟练运用。下次当你遇到看似复杂的同余条件时不妨试着列出方程也许CRT/EXCRT就是那条简洁优雅的解决路径。在算法竞赛中这是一道经典的数论题在工程领域这是一种高效的计算策略。多动手实现几次处理好所有的边界情况你就能真正驾驭这个强大的工具。