「一本通 6.4 例 4」曹冲养猪

GitHub跳转原题关系图返回列表

把猪数与猪圈的关系写成同余方程组,逐条合并剩余类:由 x=v+mt 解出 t≡(b-v)·m⁻¹ (mod a),用 pow(m,-1,a) 求逆元,模数累积成 ∏aᵢ。

OJ: roj

题目 ID: 1634

难度:普及+/提高-

标签:数论中国剩余定理同余方程扩展欧几里得python

创建: 2026-09-30 23:19

更新: 2026-10-07 13:50

形式化题目

给定 nn 个两两互质的正整数 a1,a2,…,ana_1, a_2, \dots, a_n 与对应的余数 b1,b2,…,bnb_1, b_2, \dots, b_n,求满足

x≡bi(modai)(i=1,2,…,n)x \equiv b_i \pmod{a_i} \qquad (i = 1, 2, \dots, n)

的最小正整数 xx。由于 aia_i 两两互质,解在模 ∏i=1nai\prod_{i=1}^{n} a_i 意义下唯一。

题面中的「建了 aia_i 个猪圈,剩下 bib_i 头猪没有去处」说的正是 x mod ai=bix \bmod a_i = b_i。样例给出的三条条件是 x≡1(mod3)x \equiv 1 \pmod 3、x≡1(mod5)x \equiv 1 \pmod 5、x≡2(mod7)x \equiv 2 \pmod 7,答案为 1616。

正解

思路

朴素做法与瓶颈。 最直接的想法是从 x=1x = 1 开始逐个枚举,检查是否每个 ii 都满足 x mod ai=bix \bmod a_i = b_i。枚举的步数是「答案的大小」,而答案可以达到 ∏ai\prod a_i 量级:约束 n⩽10n \leqslant 10、ai⩽1000a_i \leqslant 1000 时 ∏ai\prod a_i 接近 103010^{30},连真实数据里最小的三条件用例(a=9982,9983,9985a = 9982, 9983, 9985)答案都已是 206771281471≈2×1011206771281471 \approx 2 \times 10^{11}。所以瓶颈在于:我们在一整个剩余类里逐个试数,而这些数里的有效信息只有「落在哪个剩余类」。

关键观察:同余条件的解集仍是「一个剩余类」。 若干条 x≡b(moda)x \equiv b \pmod a 同时成立的解,仍然可以写成 x≡v(modm)x \equiv v \pmod m 的形式,mm 是这些模数的乘积。于是可以逐条合并,而不是逐个数枚举。

设已处理的条件合起来等价于 x≡v(modm)x \equiv v \pmod m,即 x=v+mtx = v + mt(tt 是任意整数)。现在加入新条件 x≡b(moda)x \equiv b \pmod a,代入得

v+mt≡b(moda)  ⟺  mt≡b−v(moda).v + mt \equiv b \pmod a \iff mt \equiv b - v \pmod a .

互质性让方程可以一步解出。 题面保证 aia_i 两两互质,而 mm 是此前若干 aja_j 的乘积,所以 gcd⁡(m,a)=1\gcd(m, a) = 1——mm 在模 aa 下存在逆元,方程有唯一解类

t≡(b−v)⋅m−1(moda).t \equiv (b - v) \cdot m^{-1} \pmod a .

把 tt 归一化到 [0,a)[0, a) 后取代表元,新的剩余类为

v′=v+mt,m′=m⋅a.v' = v + m t, \qquad m' = m \cdot a .

这一步之所以不丢解:tt 相差 aa 时 mtmt 相差 ma=m′ma = m',正是新模数,所以 t≡t0(moda)t \equiv t_0 \pmod a 的全部解给出的是同一个 x mod m′x \bmod m'。

搜索范围的塌缩。 原来的候选是 103010^{30} 个 xx,现在每条条件只需要在 [0,a)[0, a) 里定一个 tt,而且 tt 不用枚举——乘一次逆元就得到。这正是中国剩余定理的增量(逐条合并)写法。Python 的 pow(m, -1, a) 直接给出模逆元(内部就是扩展欧几里得),把「解同余方程」压成一次调用。

用样例验证推导。 三条条件逐条合并,维护不变量 x=v+mtx = v + mt:

步 新条件 (a,b)(a, b) 合并前 (v,m)(v, m) 要解的方程 解出 tt 合并后 (v,m)(v, m)
1 (3,1)(3, 1) (0,1)(0, 1) 1⋅t≡1(mod3)1 \cdot t \equiv 1 \pmod 3 11 (1,3)(1, 3)
2 (5,1)(5, 1) (1,3)(1, 3) 3t≡0(mod5)3 t \equiv 0 \pmod 5 00 (1,15)(1, 15)
3 (7,2)(7, 2) (1,15)(1, 15) 15t≡1(mod7)15 t \equiv 1 \pmod 7 11 (16,105)(16, 105)

观察重点:第 2 步解出 t=0t = 0,说明 33 与 55 的公共解里「第一个」就已经满足新条件,于是 vv 保持不变而模数涨到 1515;第 3 步因为 15≡1(mod7)15 \equiv 1 \pmod 7,逆元是 11,解出 t=1t = 1,vv 从 11 跳到 1616。最终 x≡16(mod105)x \equiv 16 \pmod{105},最小正解 1616,与样例一致。回代校验:

aia_i bib_i 16 mod ai16 \bmod a_i 是否相符
3 1 1 ✅
5 1 1 ✅
7 2 2 ✅

两个容易踩的边界。 其一,b−vb - v 可能是负数(例如 b=4b = 4、v=5v = 5):Python 的 % 总返回非负余数,代码里 % a 顺手把它归一化到 [0,a)[0, a),不需要额外分支,也不会让 vv 退化成负数。这一条在 data/input10 里就会遇到——那里的条件从 (5,4)(5, 4) 起满足 bi=ai−1b_i = a_i - 1,而合并到该步时 v=5v = 5,差值是负数。其二,合并不变量代表的是最小非负解,当所有条件都在 x=0x = 0 同时成立时 v=0v = 0,而题目要求正整数,所以代码写 return v or m:m=∏aim = \prod a_i 同样满足全部条件,且是此时最小的正解(例如 x≡2(mod2)x \equiv 2 \pmod 2 一条,应输出 22 而不是 00)。

代码

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:20
# update_at: 2026-09-30 23:20

import sys


def merge(v: int, m: int, a: int, b: int) -> tuple[int, int]:
    """并入条件 x ≡ b (mod a),返回合并后的 (余数, 模数)。

    已解部分等价于 x = v + m*t;代入得 m*t ≡ b - v (mod a)。题面保证 a 两两互质,
    而 m 是此前若干 a 的乘积,故 gcd(m, a) = 1,逆元存在,t 在模 a 下唯一。
    """
    t = (b - v) * pow(m, -1, a) % a  # `% a` 把负差值也归一化到 [0, a),新余数最小非负
    return v + m * t, m * a          # 新模数 m*a 就是这组条件的公共周期


def crt(congs: list[tuple[int, int]]) -> int:
    """按输入顺序合并同余条件 x ≡ b_i (mod a_i),返回最小正解。"""
    v, m = 0, 1  # x = 0 + 1*t 对一切 x 成立,是合并的单位元
    for a, b in congs:
        v, m = merge(v, m, a, b)
    return v or m  # v = 0 表示解是周期的整数倍,最少养猪数取 m 本身


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    n = next(data)
    congs = [(next(data), next(data)) for _ in range(n)]  # 每行一组 (猪圈数, 剩余猪数)
    print(crt(congs))


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:31
 * update_at: 2026-10-05 23:31
 */

// 曹冲养猪:解两两互质模数下的同余方程组 x ≡ b_i (mod a_i),输出最小正解。
// 做法:逐条合并剩余类(增量 CRT)。维护不变量 x = v + m*t,
// 新条件代入得 m*t ≡ b - v (mod a),由互质性用扩展欧几里得求逆元解出 t。

#include <cstdio>

typedef long long ll;
typedef __int128 lll; // 合并模数 m = ∏a_i 最大可达 1000^10 ≈ 10^30,超出 long long,用 __int128

const int MAXN = 15;

int n;
ll a[MAXN]; // a[i]:第 i 次建猪圈的个数(模数)
ll b[MAXN]; // b[i]:第 i 次剩下没去处的猪数(余数)
lll v, m;   // 已合并条件等价于 x = v + m*t,v 是当前最小非负代表元

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

// 并入一条新条件 x ≡ b_ (mod a_):把 x = v + m*t 代入得 m*t ≡ b - v (mod a)
void merge_cong(ll a_, ll b_) {
    lll a = a_, b = b_;
    lll r = ((b - v) % a + a) % a; // b - v 可能为负,归一化到 [0, a)
    lll x, y;
    exgcd(m % a, a, x, y);     // a 两两互质 ⇒ gcd(m, a) = 1,逆元必存在
    lll inv = (x % a + a) % a; // m 在模 a 下的逆元
    lll t = r * inv % a;       // t ≡ (b - v) * m^{-1} (mod a),在 [0, a) 内唯一
    v = v + m * t;             // 新的合并余数
    m = m * a;                 // 新的合并模数:这组条件的公共周期
}

// 输出 __int128
void print_lll(lll x) {
    if (x >= 10) print_lll(x / 10);
    int d = (int)(x % 10); // 每次取出的数字在 0..9,降到 int 输出
    putchar('0' + d);
}

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%lld %lld", &a[i], &b[i]);
    v = 0;
    m = 1; // 无约束:x = 0 + 1*t 对一切 x 成立,是合并的单位元
    for (int i = 1; i <= n; i++) merge_cong(a[i], b[i]);
    if (v == 0) v = m; // 所有条件在 x=0 同时成立时取周期本身,保证输出正整数
    print_lll(v);
    putchar('\n');
    return 0;
}

代码分为三层,与上面的推导一一对应:

  • merge(v, m, a, b) 负责单条合并:t = (b - v) * pow(m, -1, a) % a 就是 t≡(b−v)m−1(moda)t \equiv (b-v)m^{-1} \pmod a;返回值 v + m * t, m * a 就是 (v′,m′)(v', m')。pow 的第三个参数为 −1-1 表示求模逆元,这里依赖 gcd⁡(m,a)=1\gcd(m, a) = 1。
  • crt(congs) 维护不变量 (v,m)(v, m),初值 v, m = 0, 1 对应「无约束」(x≡0(mod1)x \equiv 0 \pmod 1 对一切 xx 成立);循环结束后 return v or m 处理「解为周期整数倍」时应当输出 mm 而不是 00。
  • solve() 只做读入与输出,(next(data), next(data)) 按题面每行的顺序取 (ai,bi)(a_i, b_i)。

复杂度

  • 时间复杂度:合并 nn 次,每次一次扩展欧几里得求逆元,代价 O(log⁡amax⁡)O(\log a_{\max});其余是次数很少的大整数乘除,位数 B=∑log⁡2ai⩽10log⁡21000≈100B = \sum \log_2 a_i \leqslant 10 \log_2 1000 \approx 100,每次 O(B)O(B)。总体

    O ⁣(n(log⁡amax⁡+B))≈O ⁣(nlog⁡∏ai),O\!\left(n \left(\log a_{\max} + B\right)\right) \approx O\!\left(n \log \textstyle\prod a_i\right),

    在约束下是「1010 次 exgcd 加少量百位大数运算」的量级。

  • 空间复杂度:只保存 vv、mm 和 nn 组输入,O(n+B)O(n + B)。

  • 对比朴素枚举的 O(ans⋅n)O(\text{ans} \cdot n)(ans\text{ans} 可达 103010^{30}),这是本质上的降级;求逆元没有用「枚举 tt 试 aa 次」写,所以整体仍是 O(nlog⁡)O(n \log) 而不是 O(na)O(na)。

总结

  • 「aia_i 个一圈剩下 bib_i 头」就是同余式 x≡bi(modai)x \equiv b_i \pmod{a_i},题目要求同余方程组的最小正解。
  • 逐条合并的核心是维护不变量 x≡v(modm)x \equiv v \pmod m:把旧解写成 v+mtv + mt 代入新条件,用 t≡(b−v)m−1(moda)t \equiv (b-v)m^{-1} \pmod a 更新 (v,m)→(v+mt, ma)(v, m) \to (v + mt,\ ma)。
  • aia_i 两两互质 ⇒ gcd⁡(m,a)=1\gcd(m, a) = 1 ⇒ 逆元存在,合并永远有解,不需要处理一般 CRT 的无解分支。
  • 两个实现细节:% a 处理负差值(Python 自动归一化),v or m 处理「输出必须为正」。