中国剩余定理
韩信点兵
打仗需要清点士兵,但士兵太多了,一个一个数太慢,于是韩信用了个聪明的方法:
- 让士兵们 \(3\) 人一排站好,结果发现最后剩下 \(2\) 个人。
- 让他们 \(5\) 人一排,结果剩下 \(3\) 个人。
- 最后,让他们 \(7\) 人一排,又剩下 \(2\) 个人。
问题:在不一个一个数的情况下,能知道至少带了多少个兵吗?
这个问题用数学语言来说就是:求一个数 \(x\),使得:
- \(x\) 除以 \(3\) 余 \(2\),写作 \(x \equiv 2 \pmod {3}\)
- \(x\) 除以 \(5\) 余 \(3\),写作 \(x \equiv 3 \pmod {5}\)
- \(x\) 除以 \(7\) 余 \(2\),写作 \(x \equiv 2 \pmod {7}\)
中国剩余定理就是解决这类问题的通用方法。
这个定理的精髓思想是:把复杂的问题分解成几个小问题,分别解决后,再巧妙地组合起来。
先找一个数,它要满足:除以 \(3\) 余 \(1\),同时是 \(5\) 和 \(7\) 的倍数(这样它除以 \(5\) 或 \(7\) 的时候,余数就是 \(0\))。这个数首先得是 \(5 \times 7 = 35\) 的倍数。\(35\) 除以 \(3\) 余 \(2\),不行。\(35 \times 2 = 70\),\(70\) 除以 \(3\) 余 \(1\)(这个过程实际上就是在求 \(35\) 模 \(3\) 的逆元),这样就找到了第一个“零件”。
找一个数,它要满足:除以 \(5\) 余 \(1\),同时是 \(3\) 和 \(7\) 的倍数。这个数是 \(3 \times 7 = 21\) 的倍数。\(21\) 除以 \(5\) 余 \(1\),\(21\) 就是第二个“零件”。
找一个数,它要满足:除以 \(7\) 余 \(1\),同时是 \(3\) 和 \(5\) 的倍数。这个数是 \(3 \times 5 = 15\) 的倍数。\(15\) 除以 \(7\) 余 \(1\),\(15\) 就是第三个“零件”。
现在有了三个零件 \(70,21,15\),其中 \(70\) 能被 \(5\) 和 \(7\) 整除,除以 \(3\) 余 \(1\)。\(21\) 能被 \(3\) 和 \(7\) 整除,除以 \(5\) 余 \(1\)。\(15\) 能被 \(3\) 和 \(5\) 整除,除以 \(7\) 余 \(1\)。
回到最初的问题:除以 \(3\) 余 \(2\),除以 \(5\) 余 \(3\),除以 \(7\) 余 \(2\)。
把“零件”和想要的“余数”乘起来:
- 对于“除以 \(3\) 余 \(2\)”,用零件 \(1\):\(70 \times 2 = 140\)。这个 \(140\),它除以 \(5\) 和 \(7\) 都余 \(0\),但除以 \(3\) 余几呢?因为 \(70\) 除以 \(3\) 余 \(1\),所以 \(140\) 除以 \(3\) 就余 \(2\)。正好满足第一个条件。
- 对于“除以 \(5\) 余 \(3\)”,用零件 \(2\):\(21 \times 3 = 63\)。这个 \(63\),它除以 \(3\) 和 \(7\) 都余 \(0\),但除以 \(5\) 余几呢?因为 \(21\) 除以 \(5\) 余 \(1\),所以 \(63\) 除以 \(5\) 就余 \(3\)。正好满足第二个条件。
- 对于“除以 \(7\) 余 \(2\)”,用零件 \(3\):\(15 \times 2 = 30\)。这个 \(30\),它除以 \(3\) 和 \(5\) 都余 \(0\),但除以 \(7\) 余几呢?因为 \(15\) 除以 \(7\) 余 \(1\),所以 \(30\) 除以 \(7\) 就余 \(2\)。正好满足第三个条件。
现在,把这三个“半成品”加起来:\(140+63+30=233\)。这个 \(233\) 就是一个满足所有条件的答案。
\(233\) 是一个可能的答案。但问题是至少有多少兵。
如果 \(233\) 满足条件,那么 \(233+(3 \times 5 \times 7)\) 这个数是不是也满足条件?\(3 \times 5 \times 7 = 105\),\(105\) 能同时被 \(3,5,7\) 整除,所以 \(233+105\) 这个数除以 \(3,5,7\) 的余数和 \(233\) 是完全一样的。
所以,所有满足条件的数是 \(233, 233+105, 233+2 \times 105, \dots\)。为了找到最小的那个正整数,用 \(233\) 除以 \(105\),看余数是多少:\(233 \div 105 = 2 \cdots 23\)。所以韩信至少有 \(23\) 个兵。
重要前提:这个方法有一个要求,就是所有的除数(比如前面的 \(3,5,7\))必须两两互质,也就是它们之间除了 \(1\) 没有别的公约数。
选择题:一个班学生分组做游戏,如果每组三人就多两人,每组五人就多三人,每组七人就多四人,问这个班的学生人数 n 在以下哪个区间?已知 \(n \lt 60\)。
- A. \(30 \lt n \lt 40\)
- B. \(40 \lt n \lt 50\)
- C. \(50 \lt n \lt 60\)
- D. \(20 \lt n \lt 30\)
答案
C。
这是一个同余方程组。
- \(n \equiv 2 \pmod 3\)
- \(n \equiv 3 \pmod 5\)
- \(n \equiv 4 \pmod 7\)
方法一:逐步筛选法
可以从约束最强的条件(除数最大)开始,列出所有可能的 n 值,然后逐一验证。
- 满足“每组七人就多四人”且 \(n \lt 60\) 的数有:4, 11, 18, 25, 32, 39, 46, 53。
- 从上面的数中,找出满足“每组五人就多三人”(即 n 的个位数是 3 或 8)的数:18, 53。
- 最后,从剩下的数中,找出满足“每组三人就多两人”的数:53。
方法二:巧用倍数法
观察三个同余方程,尝试将每个方程乘以 2,可以得到:
- \(2n \equiv 2 \times 2 \equiv 4 \equiv 1 \pmod 3\)
- \(2n \equiv 3 \times 2 \equiv 6 \equiv 1 \pmod 5\)
- \(2n \equiv 4 \times 2 \equiv 8 \equiv 1 \pmod 7\)
可以发现 2n 除以 3,5,7 的余数都是 1。这意味着 2n-1 是 3,5,7 的公倍数。
为了求 n 的最小值,需要求 3,5,7 的最小公倍数。因为 3,5,7 都是质数,所以它们的最小公倍数是 3 × 5 × 7 = 105。
因此,2n-1 的值可以是 105,210,315,……。取最小值,令 2n-1 = 105,解得 2n = 106,所以 n = 53。
方法三:中国剩余定理
计算所有模数的乘积 M = 3×5×7 = 105。
\(M_i\) 是 M 除以当前方程模数 \(m_i\) 的结果。
- \(M_1 = M / 3 = 105 / 3 = 35\)
- \(M_2 = M / 5 = 105 / 5 = 21\)
- \(M_3 = M / 7 = 105 / 7 = 15\)
模逆元 \(t_i\) 需要满足 \((M_i \times t_i) \equiv 1 \pmod {m_i}\)。可以求得 \(t_1 = 2, \ t_2 = 1, \ t_3 = 1\)。
根据公式 \(n \equiv (a_1 M_1 t_1 + a_2 M_2 t_2 + a_3 M_3 t_3) \pmod M\),其中 \(a_i\) 是每个方程的余数。可以求得 \(n \equiv 263 \equiv 53 \pmod 105\)。
在 \(n \lt 60\) 的条件下,满足该方程的解只有一个,即 n = 53。
习题:P1495 【模板】中国剩余定理(CRT)/ 曹冲养猪
解题思路
注意:因为中国剩余定理只需要模数互质而不需要模数都是质数,所以求逆元需要用扩展欧几里得算法。
参考代码
#include <cstdio>
using ll = long long;
const int N = 15;
int a[N], b[N];
ll mul(ll x, ll y, ll mod) {
ll res = 0;
while (y > 0) {
if (y & 1) res = (res + x) % mod;
x = (x + x) % mod;
y >>= 1;
}
return res;
}
ll exgcd(ll a, ll b, ll &x, ll &y) {
if (b == 0) {
x = 1; y = 0; return a;
}
ll g = exgcd(b, a % b, x, y);
ll t = x; x = y; y = t - a / b * y;
return g;
}
ll inverse(ll n, ll m) { // 计算n在模m下的乘法逆元
ll x, y;
ll g = exgcd(n, m, x, y);
return (x % m + m) % m; // 保证返回的解非负
}
int main()
{
int n; scanf("%d", &n);
ll m = 1;
for (int i = 1; i <= n; i++) {
scanf("%d%d", &a[i], &b[i]);
m *= a[i]; // 计算所有模数的乘积
}
ll ans = 0;
for (int i = 1; i <= n; i++) { // 对每个同余方程进行计算
ll mi = m / a[i];
ll ti = inverse(mi, a[i]);
// 注意这里直接乘法可能会溢出long long范围
ans = (ans + mul(mul(mi, b[i], m), ti, m)) % m;
}
printf("%lld\n", ans % m);
return 0;
}
扩展中国剩余定理
中国剩余定理有一个原则:所有的模数必须两两互质。
但现实中的问题不一定都满足这一点,比如:
- \(x \equiv 1 \pmod {4}\)
- \(x \equiv 3 \pmod {6}\)
这里的模数是 \(4\) 和 \(6\),它们不互质(有公约数 \(2\))。这时候,之前的方法就行不通了。
扩展中国剩余定理,就是专门用来解决模数不互质情况下的同余方程组的。
核心思想:两两合并,逐步消元。
既然不能像之前那样“一步到位”,那就换个思路:一次只处理两个方程,把它们合并成一个等价的、新的方程。然后用这个方程再去和第三个方程合并,像滚雪球一样,直到最后只剩下一个方程,答案就出来了。
这个过程就像二元一次方程组里的“代入消元法”,非常巧妙。
以上面的例子为例:
第一步:把第一个方程“代数化”
\(x \equiv 1 \pmod {4}\) 用熟悉的代数语言来说就是:\(x\) 是一个 \(4\) 的倍数再加上 \(1\) 的数。所以,可以写成:\(x = 4k + 1\),其中 \(k\) 是某个整数。
第二步:代入第二个方程
现在,把这个 \(x\) 的表达式代入到第二个方程 \(x \equiv 3 \pmod{6}\) 中:\(4k+1 \equiv 3 \pmod{6}\)。
这里成功消掉了 \(x\),得到了一个只关于 \(k\) 的新同余方程。
第三步:解出 \(k\)
解这个关于 \(k\) 的方程:\(4k \equiv 2 \pmod{6}\)。
这个方程是什么意思呢?它的意思是 \(4k-2\) 必须是 \(6\) 的倍数。这个方程需要用到扩展欧几里得算法来解,可以求出一组解 \(k=2\)。
第四步:把 \(k\) 代回去,得到 \(x\) 的最终形式
已经找到了一个解 \(k=2\),现在把它代回到第一步的 \(x\) 的表达式中:\(x = 4k+1 = 4 \times 2 + 1 = 9\)。
这个 \(9\) 就是满足前两个方程的一个特解。所有解的形式是 \(x \equiv 9 \pmod{lcm(4,6)}\),其中 \(lcm\) 是最小公倍数。\(lcm(4,6)=12\)。
第五步:写出合并后的新方程
\(x \equiv 9 \pmod{12}\)。这就是把两个方程成功地合并成了一个等价的方程。
这个新方程包含了前两个方程的所有信息。它的最小非负整数解就是 \(9\)。
如果还有第三个方程,就继续拿 \(x \equiv 9 \pmod{12}\) 和它进行新一轮的合并。
习题:P4777 【模板】扩展中国剩余定理(EXCRT)
参考代码
#include <cstdio>
using ll = long long;
const int N = 100005;
ll a[N], b[N];
ll mul(ll x, ll y, ll m) {
ll res = 0;
while (y > 0) {
if (y & 1) res = (res + x) % m;
x = (x + x) % m;
y >>= 1;
}
return res;
}
ll exgcd(ll A, ll B, ll &x, ll &y) {
if (B == 0) {
x = 1; y = 0; return A;
}
ll g = exgcd(B, A % B, x, y);
ll t = x; x = y; y = t - A / B * y;
return g;
}
int main()
{
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%lld%lld", &a[i], &b[i]);
}
// 初始解为第一个方程
ll cur_a = a[1], cur_b = b[1];
// 循环合并后续的方程
for (int i = 2; i <= n; i++) {
// 需要解 x ≡ cur_b (mod cur_a) 和 x ≡ b[i] (mod a[i])
// x = cur_a * k + cur_b
// 代入第二个方程:cur_a * k + cur_b ≡ b[i] (mod a[i])
// 整理得:cur_a * k ≡ b[i] - cur_b (mod a[i])
// 求解线性同余方程 Ak ≡ B (mod M)
// 其中 A = cur_a, B = b[i] - cur_b, M = a[i]
ll A = cur_a, B = b[i] - cur_b, M = a[i];
B = (B % M + M) % M;
// Ak + My = B
ll k, y;
ll g = exgcd(A, M, k, y);
M /= g;
k = mul(k, B / g, M);
k = (k % M + M) % M;
ll tmp_a = cur_a;
cur_a = cur_a / g * a[i];
cur_b = (mul(tmp_a, k, cur_a) + cur_b) % cur_a;
}
printf("%lld\n", cur_b);
return 0;
}

浙公网安备 33010602011771号