逐条合并同余式:代入得线性同余方程 m₁k ≡ a₂-a₁ (mod m₂),gcd 不整除差值即无解,约去公因子后求模逆元得到最小非负 k,模数合并为 lcm。
OJ: roj
题目 ID: 1635
难度:提高+/省选-
标签:数论中国剩余定理扩展欧几里得python
创建: 2026-09-30 23:20
更新: 2026-10-07 13:50
形式化题目
给定
求满足全部同余式的最小非负整数
题面不保证
样例
| 步骤 | 已合并的同余式 | 含义 |
|---|---|---|
| 初始 | 没有约束,任何 |
|
| 合并 |
解集 |
|
| 合并 |
两个解集的交集,最小元素是 |
所以答案是 31。表中第三行说明本题的目标可以改写成:把
正解
思路
先看朴素做法为什么不行。 最直接的想法是枚举
但 data/6.in 一个文件就有
关键观察:两条同余式可以合并成一条。 记状态
把合并化成线性同余方程。 设
这就是标准形式
- 令
。方程有解 。这里即 必须整除 ; 否则两条式子互相矛盾,整组无解,直接输出 。 时两边同除 ,得 。 此时 ,系数可逆: 。 - 取
的最小非负代表,新的状态就是 。
为什么这样得到的
不用手写扩展欧几里得。 Python 的 pow(x, -1, m) 直接给出模逆元,
所以 merge 只有三行:gcd 判整除、约分求逆元、算出新状态。
样例的逐步合并过程如下,表中
| 步骤 | 待合并的同余式 | 合并前 |
新 |
合并后 |
判定 | ||
|---|---|---|---|---|---|---|---|
| 0 | 无 | — | — | — | — | 初始, |
|
| 1 | 1 | 8 | 7 | 合并成功 | |||
| 2 | 1 | 11 | 3 | 合并成功 |
第 1 步的
两条最容易被漏掉的判断。 模数不互质时结果分两种,用反例区分:
| 例子 | 合并过程 | 结论 |
|---|---|---|
| 合并为 |
||
| 无解,输出 |
第一行说明不互质不等于无解:
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(已是最小非负解)"]
图中只有一条「失败出口」:
代码
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++ 版(同一算法):
/**
* 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加一次模逆元,。 - 单组
,全部数据 。 与 的数值大小无关,这是相对枚举的本质优势。 - 额外空间:单组
( pairs列表;状态本身只有两个整数), 另有的 read()缓冲和的输出列表。 - 实测
data/6.in(15 组、453043 条同余式)0.16–0.20s、峰值内存 51.5MB,时限 1000ms、内存 128MB 内充分。
总结
- 只要读到「求满足
的最小 」,就先问一句: 互质吗? 本题不互质,所以要用可判无解的合并式 CRT,而不是乘积公式。 - 核心公式只有一条:把
代入第二条得 ; 不整除差值即无解,否则约去公因子求逆元,新模数是 。 - 每步都取最小非负的
,新余数就一定落在 内, 所以最后直接输出,不必再取最小值。 - 多组数据要读到 EOF;某组已判无解时仍要把该组剩下的输入读完,否则后续数据会错位。
图示解析
下面这张图把整道题的路线压成一条主线:从
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),已是最小非负解两点值得注意:一是唯一的失败判定在合并内部(