把猪数与猪圈的关系写成同余方程组,逐条合并剩余类:由 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
形式化题目
给定
的最小正整数
题面中的「建了
正解
思路
朴素做法与瓶颈。 最直接的想法是从
关键观察:同余条件的解集仍是「一个剩余类」。 若干条
设已处理的条件合起来等价于
互质性让方程可以一步解出。 题面保证
把
这一步之所以不丢解:
搜索范围的塌缩。 原来的候选是 pow(m, -1, a) 直接给出模逆元(内部就是扩展欧几里得),把「解同余方程」压成一次调用。
用样例验证推导。 三条条件逐条合并,维护不变量
| 步 | 新条件 |
合并前 |
要解的方程 | 解出 |
合并后 |
|---|---|---|---|---|---|
| 1 | |||||
| 2 | |||||
| 3 |
观察重点:第 2 步解出
| 是否相符 | |||
|---|---|---|---|
| 3 | 1 | 1 | ✅ |
| 5 | 1 | 1 | ✅ |
| 7 | 2 | 2 | ✅ |
两个容易踩的边界。 其一,% 总返回非负余数,代码里 % a 顺手把它归一化到 data/input10 里就会遇到——那里的条件从 return v or m:
代码
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++ 版(同一算法):
/**
* 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就是;返回值 v + m * t, m * a就是。 pow的第三个参数为表示求模逆元,这里依赖 。 crt(congs)维护不变量,初值 v, m = 0, 1对应「无约束」(对一切 成立);循环结束后 return v or m处理「解为周期整数倍」时应当输出而不是 。 solve()只做读入与输出,(next(data), next(data))按题面每行的顺序取。
复杂度
-
时间复杂度:合并
次,每次一次扩展欧几里得求逆元,代价 ;其余是次数很少的大整数乘除,位数 ,每次 。总体 在约束下是「
次 exgcd 加少量百位大数运算」的量级。 -
空间复杂度:只保存
、 和 组输入, 。 -
对比朴素枚举的
( 可达 ),这是本质上的降级;求逆元没有用「枚举 试 次」写,所以整体仍是 而不是 。
总结
- 「
个一圈剩下 头」就是同余式 ,题目要求同余方程组的最小正解。 - 逐条合并的核心是维护不变量
:把旧解写成 代入新条件,用 更新 。 两两互质 ⇒ ⇒ 逆元存在,合并永远有解,不需要处理一般 CRT 的无解分支。 - 两个实现细节:
% a处理负差值(Python 自动归一化),v or m处理「输出必须为正」。