中国剩余定理

韩信点兵

打仗需要清点士兵,但士兵太多了,一个一个数太慢,于是韩信用了个聪明的方法:

  1. 让士兵们 \(3\) 人一排站好,结果发现最后剩下 \(2\) 个人。
  2. 让他们 \(5\) 人一排,结果剩下 \(3\) 个人。
  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 值,然后逐一验证。

  1. 满足“每组七人就多四人”且 \(n \lt 60\) 的数有:4, 11, 18, 25, 32, 39, 46, 53。
  2. 从上面的数中,找出满足“每组五人就多三人”(即 n 的个位数是 3 或 8)的数:18, 53。
  3. 最后,从剩下的数中,找出满足“每组三人就多两人”的数: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;
}
posted @ 2025-07-11 06:38  RonChen  阅读(81)  评论(0)    收藏  举报