形式化题目

给定 nn 条同余式

x≡ai(modmi),i=1,2,…,n,x \equiv a_i \pmod{m_i},\qquad i=1,2,\dots,n,

求满足全部同余式的最小非负整数 xx;若不存在这样的 xx,输出 −1-1。 同一次运行会依次给出多组互相独立的 (n,a1,m1,…,an,mn)(n,a_1,m_1,\dots,a_n,m_n),每组各自回答一次。

题面不保证 mim_i 两两互质,这是本题与教科书上的中国剩余定理最大的差别; 真实数据里就出现了同一组内 m1=m2=40m_1=m_2=40 而 aa 不同、以及 m=4m=4 与 m=6m=6 这样的组合。

样例 n=2n=2,两条式子为 x≡7(mod8)x\equiv7\pmod 8 与 x≡9(mod11)x\equiv9\pmod{11}:

步骤 已合并的同余式 含义
初始 x≡0(mod1)x\equiv0\pmod 1 没有约束,任何 xx 都满足
合并 x≡7(mod8)x\equiv7\pmod 8 x≡7(mod8)x\equiv7\pmod 8 解集 {…,7,15,23,31,… }\{\dots,7,15,23,31,\dots\}
合并 x≡9(mod11)x\equiv9\pmod{11} x≡31(mod88)x\equiv31\pmod{88} 两个解集的交集,最小元素是 3131

所以答案是 31。表中第三行说明本题的目标可以改写成:把 nn 条同余式折叠成一条。

正解

思路

先看朴素做法为什么不行。 最直接的想法是枚举 xx:由前两条式子可知解一定落在 [0,lcm⁡(m1,…,mn))[0,\operatorname{lcm}(m_1,\dots,m_n)) 内,逐个试即可。或者稍微聪明一点,由 x≡a1(modm1)x\equiv a_1\pmod{m_1} 写成 x=a1+km1x=a_1+km_1,只枚举 kk,再检验其余同余式。样例 lcm⁡(8,11)=88\operatorname{lcm}(8,11)=88 只要试不到 88 次, 看起来完全可行。

但 nn 最大到 10510^5,模数几乎互质时 lcm⁡\operatorname{lcm} 就是 10510^5 个数的乘积,位数以十万计, 枚举量是天文数字。真实数据里仅 data/6.in 一个文件就有 453043453043 条同余式,按这个做法一组都跑不完。 瓶颈不在「检验一条同余式太慢」,而在于把「满足所有条件」当成了「逐个试」—— 所有解其实构成一个同余类,本可以用代数方法直接算出来。

关键观察:两条同余式可以合并成一条。 记状态 (r,m)(r,m) 表示同余式 x≡r(modm)x\equiv r\pmod m, 则「同时满足 (r1,m1)(r_1,m_1) 与 (a2,m2)(a_2,m_2)」与某一条 x≡r(modlcm⁡(m1,m2))x\equiv r\pmod{\operatorname{lcm}(m_1,m_2)} 的解集完全相同。只要能算出这个新的 rr,就可以从单位元 (0,1)(0,1)(x≡0(mod1)x\equiv0\pmod1 恒成立) 出发,把 nn 条式子从左到右折叠成一条,最后的 rr 就是答案。

把合并化成线性同余方程。 设 x=r1+m1kx=r_1+m_1k(这是「满足第一条」的全部解), 代入第二条:

r1+m1k≡a2(modm2)⟺m1k≡a2−r1(modm2).r_1+m_1k\equiv a_2\pmod{m_2}\quad\Longleftrightarrow\quad m_1k\equiv a_2-r_1\pmod{m_2}.

这就是标准形式 ak≡b(modn)ak\equiv b\pmod n(a=m1, b=a2−r1, n=m2a=m_1,\ b=a_2-r_1,\ n=m_2),有现成的判据与解法:

  1. 令 g=gcd⁡(a,n)g=\gcd(a,n)。方程有解   ⟺  g∣b\iff g\mid b。这里即 g=gcd⁡(m1,m2)g=\gcd(m_1,m_2) 必须整除 a2−r1a_2-r_1; 否则两条式子互相矛盾,整组无解,直接输出 −1-1。
  2. g∣bg\mid b 时两边同除 gg,得 m1gk≡a2−r1g(modm2g)\dfrac{m_1}{g}k\equiv\dfrac{a_2-r_1}{g}\pmod{\dfrac{m_2}{g}}。 此时 gcd⁡ ⁣(m1g,m2g)=1\gcd\!\left(\dfrac{m_1}{g},\dfrac{m_2}{g}\right)=1,系数可逆: k0=a2−r1g⋅(m1g)−1 mod m2gk_0=\dfrac{a_2-r_1}{g}\cdot\left(\dfrac{m_1}{g}\right)^{-1}\bmod\dfrac{m_2}{g}。
  3. 取 k≡k0(modm2g)k\equiv k_0\pmod{\frac{m_2}{g}} 的最小非负代表,新的状态就是 (r1+m1k, m1gm2)\left(r_1+m_1k,\ \dfrac{m_1}{g}m_2\right)。

为什么这样得到的 rr 已经是最小解。 kk 取模 m2g\frac{m_2}{g} 的最小非负代表后, r1+m1kr_1+m_1k 必落在 [0,lcm⁡)[0,\operatorname{lcm}) 内,正是合并后同余式的最小非负解, 也必然是原系统的最小非负解(否则减去一个 lcm⁡\operatorname{lcm} 会得到更小的解)。 所以全部合并完后直接输出 rr 即可,不需要再取一次最小值。

不用手写扩展欧几里得。 Python 的 pow(x, -1, m) 直接给出模逆元, 所以 merge 只有三行:gcd 判整除、约分求逆元、算出新状态。

样例的逐步合并过程如下,表中 (r,m)(r,m) 是每一步之后的状态:

步骤 待合并的同余式 合并前 (r,m)(r,m) g=gcd⁡(m,mi)g=\gcd(m,m_i) mi/gm_i/g 新 kk 合并后 (r,m)(r,m) 判定
0 无 — — — — (0,1)(0,1) 初始,x≡0(mod1)x\equiv 0\pmod 1 恒成立
1 x≡7(mod8)x\equiv 7\pmod{8} (0,1)(0,1) 1 8 7 (7,8)(7,8) 合并成功
2 x≡9(mod11)x\equiv 9\pmod{11} (7,8)(7,8) 1 11 3 (31,88)(31,88) 合并成功

第 1 步的 k=7k=7 把「x≡0(mod1)x\equiv0\pmod1 的解」{x=0+k⋅1}\{x=0+k\cdot1\} 变成 x=0+7=7x=0+7=7; 第 2 步解 8k≡9−7=2(mod11)8k\equiv 9-7=2\pmod{11} 得 k=3k=3(因为 8−1≡7(mod11)8^{-1}\equiv7\pmod{11},2×7=14≡32\times7=14\equiv3), 于是 r=7+8×3=31r=7+8\times3=31。

两条最容易被漏掉的判断。 模数不互质时结果分两种,用反例区分:

例子 合并过程 结论
x≡0(mod4)x\equiv0\pmod4、x≡2(mod6)x\equiv2\pmod6 g=gcd⁡(4,6)=2∣(2−0)g=\gcd(4,6)=2\mid(2-0),k=(1)⋅2−1 mod 3=2k=(1)\cdot 2^{-1}\bmod3=2 合并为 x≡8(mod12)x\equiv8\pmod{12},有解
x≡0(mod4)x\equiv0\pmod4、x≡1(mod6)x\equiv1\pmod6 g=2∤(1−0)g=2\nmid(1-0) 无解,输出 −1-1

第一行说明不互质不等于无解:x=8x=8 同时满足 8 mod 4=08\bmod4=0 与 8 mod 6=28\bmod6=2。 第二行说明不互质也不等于有解:x≡0(mod4)x\equiv0\pmod4 要求 xx 是偶数,x≡1(mod6)x\equiv1\pmod6 要求 xx 是奇数, 矛盾。这正是必须显式检查 g∣bg\mid b 的原因,也是不能直接套互质版 CRT 的原因—— 在 m1=4m_1=4 对模 66 不可逆时,乘积公式连第一步都算不下去。

merge 内部的全部走向可以概括成下图。

graph TD
    A["输入 state=(r1,m1) 与 (a2,m2)"] --> B["g = gcd(m1, m2)"]
    B --> C{"(a2-r1) mod g == 0 ?"}
    C -- "否" --> D["返回 None:两条式子矛盾,整组无解"]
    C -- "是" --> E["k = (a2-r1)/g * inv(m1/g) mod (m2/g)"]
    E --> F["返回 (r1+m1*k, m1/g*m2)"]
    F --> G{"还有同余式吗?"}
    G -- "有" --> A
    G -- "没有" --> H["输出 r1(已是最小非负解)"]

图中只有一条「失败出口」:g∤(a2−r1)g\nmid(a_2-r_1)。只要它不触发,合并就一定能继续, 并且每次合并都把两条式子的解集精确地压成一条,信息没有丢失。

代码

Python 版:

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 23:30
# update_at: 2026-09-30 23:30

import sys
from math import gcd

NO_SOLUTION = -1  # 同余式组无解时的输出


def merge(state: tuple[int, int], r2: int, m2: int) -> tuple[int, int] | None:
    """把 x ≡ r2 (mod m2) 合并进 state = (r1, m1),返回等价的最小同余式;无解返回 None。

    由 r1 + m1*k ≡ r2 得线性同余方程 m1*k ≡ r2-r1 (mod m2):
    g = gcd(m1,m2) 不整除差值时无解,否则约去 g 后 m1/g 与 m2/g 互质,可直接求逆。
    k 取模 m2/g 的最小非负解,于是新余数 r1+m1*k 已落在 [0, lcm) 内,天然最小。
    """
    r1, m1 = state
    g = gcd(m1, m2)
    diff = r2 - r1
    if diff % g:
        return None
    k = diff // g * pow(m1 // g, -1, m2 // g) % (m2 // g)
    return r1 + m1 * k, m1 // g * m2


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    out: list[str] = []
    for n in data:
        # 模数两两不互质,套不了 CRT 的乘积公式,只能逐条合并同余式。
        pairs = [(next(data), next(data)) for _ in range(n)]
        state: tuple[int, int] | None = (0, 1)  # 空同余式组 x ≡ 0 (mod 1) 恒成立
        for m, a in pairs:
            if state is not None:  # 已判定无解就跳过计算,但仍要读完本组数据
                state = merge(state, a, m)
        out.append(str(NO_SOLUTION if state is None else state[0]))
    sys.stdout.write("\n".join(out) + "\n")


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-05 23:47
 * update_at: 2026-10-05 23:47
 */

#include <iostream>
#include <numeric>
using namespace std;

typedef long long ll;

// 扩展欧几里得:求 a*x + b*y = gcd(a,b),返回 gcd(a,b)。
ll exgcd(ll a, ll b, ll &x, ll &y) {
    if (b == 0) {
        x = 1;
        y = 0;
        return a;
    }
    ll x1, y1;
    ll g = exgcd(b, a % b, x1, y1);
    x = y1;
    y = x1 - a / b * y1;
    return g;
}

// 求 a 在模 m (>1) 下的逆元,调用前保证 gcd(a, m) == 1。
ll mod_inverse(ll a, ll m) {
    ll x, y;
    exgcd(a, m, x, y);
    return (x % m + m) % m;
}

// 把 x ≡ a2 (mod m2) 合并进当前同余式 x ≡ r1 (mod m1)。
// 有解时更新 r1、m1 为新同余式并返回 true;矛盾无解时返回 false。
bool merge_congruence(ll &r1, ll &m1, ll a2, ll m2) {
    ll g = gcd(m1, m2);
    ll diff = a2 - r1;
    if (diff % g != 0) {
        return false; // gcd 不整除差值,两条同余式矛盾
    }
    ll n = m2 / g; // 约去公因子后 m1/g 与 n 互质,可直接求逆元
    ll b = diff / g;
    b = (b % n + n) % n;
    ll k = 0;
    if (n > 1) {
        ll inv = mod_inverse((m1 / g) % n, n);
        k = b * inv % n; // 取模 m2/g 的最小非负 k
    }
    r1 = r1 + m1 * k; // 新余数已落在 [0, lcm) 内,天然最小
    m1 = m1 / g * m2; // 新模数变成 lcm(m1, m2)
    return true;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    ll n;
    while (cin >> n) {
        ll r = 0; // 当前同余式 x ≡ r (mod m),初值为恒成立式 x ≡ 0 (mod 1)
        ll m = 1;
        bool ok = true;
        for (ll i = 1; i <= n; i++) {
            ll mi, ai;
            cin >> mi >> ai;
            if (ok) {
                // 已判定无解仍要读完本组剩余输入,否则后续数据会错位。
                ok = merge_congruence(r, m, ai, mi);
            }
        }
        if (ok) {
            cout << r << "\n";
        } else {
            cout << -1 << "\n";
        }
    }

    return 0;
}

复杂度

  • 每条同余式一次 merge:一次 gcd 加一次模逆元,O(log⁡max⁡imi)O(\log\max_i m_i)。
  • 单组 O ⁣(nlog⁡max⁡imi)O\!\left(n\log\max_i m_i\right),全部数据 O ⁣(∑n⋅log⁡max⁡imi)O\!\left(\sum n\cdot\log\max_i m_i\right)。 与 lcm⁡\operatorname{lcm} 的数值大小无关,这是相对枚举的本质优势。
  • 额外空间:单组 O(n)O(n)(pairs 列表;状态本身只有两个整数), 另有 O(输入长度)O(\text{输入长度}) 的 read() 缓冲和 O(组数)O(\text{组数}) 的输出列表。
  • 实测 data/6.in(15 组、453043 条同余式)0.16–0.20s、峰值内存 51.5MB,时限 1000ms、内存 128MB 内充分。

总结

  • 只要读到「求满足 x≡ai(modmi)x\equiv a_i\pmod{m_i} 的最小 xx」,就先问一句:mim_i 互质吗? 本题不互质,所以要用可判无解的合并式 CRT,而不是乘积公式。
  • 核心公式只有一条:把 x=r1+m1kx=r_1+m_1k 代入第二条得 m1k≡a2−r1(modm2)m_1k\equiv a_2-r_1\pmod{m_2}; gcd⁡\gcd 不整除差值即无解,否则约去公因子求逆元,新模数是 lcm⁡\operatorname{lcm}。
  • 每步都取最小非负的 kk,新余数就一定落在 [0,lcm⁡)[0,\operatorname{lcm}) 内, 所以最后直接输出,不必再取最小值。
  • 多组数据要读到 EOF;某组已判无解时仍要把该组剩下的输入读完,否则后续数据会错位。

图示解析

下面这张图把整道题的路线压成一条主线:从 nn 条同余式出发,反复做同一个动作,直到剩一条。

text
n 条同余式  x ≡ a_i (mod m_i)
   │  初始 state = (0, 1)          # x ≡ 0 (mod 1) 恒成立,是合并的单位元
   ▼
┌─────────────────── 对每条 (a_i, m_i) 重复 ───────────────────┐
│  state = (r, m)                                              │
│     │  解 r + m·k ≡ a_i (mod m_i)                            │
│     ├─ g = gcd(m, m_i)                                       │
│     │                                                        │
│     ├─ g ∤ (a_i - r)  ──────────► state = None(无解定型)    │
│     │                                                        │
│     └─ g | (a_i - r)                                         │
│           k = (a_i-r)/g · (m/g)⁻¹ mod (m_i/g)   # 最小非负     │
│           state = (r + m·k,  m/g · m_i)         # 模数变 lcm  │
└──────────────────────────────────────────────────────────────┘
   │
   ▼
state = None ?  ──是──►  输出 -1
   │
   否
   ▼
输出 r    # r ∈ [0, lcm),已是最小非负解

两点值得注意:一是唯一的失败判定在合并内部(gg 是否整除差值), 不需要额外的前置检查;二是模数在合并过程中不断变成 lcm⁡\operatorname{lcm}, 所以「解的上界」增长极快,这正是枚举法失效、而代数合并仍然只花 O(log⁡m)O(\log m) 的原因。