把 t 次跳跃后相遇写成同余方程 (m-n)t≡y-x(mod L),用扩展欧几里得求特解,再按周期 L/gcd 归一化出最小非负解;gcd 不整除差值则无解。
OJ: roj
题目 ID: 1631
难度:普及+/提高-
标签:数论扩展欧几里得同余方程python
创建: 2026-09-30 23:07
更新: 2026-10-07 13:50
形式化题目
环长为
若这样的 Impossible。数据范围:
正解
思路
朴素做法与瓶颈。 最直接的想法是模拟:每一步让两只青蛙各前进一次,检查位置是否相同。单次跳跃后 A 领先 B 的距离减少
把相遇写成同余方程。 相遇条件是两者位置模
移项,未知数只有
这是一个标准的线性同余方程
用 exgcd 解同余方程。 设
- 有解判定:同余方程
有解当且仅当 。直观理解: 必须是 的倍数,而 同时整除 与 ,所以 必须整除 。若 ,输出 Impossible。 - 求特解:当
时,把 Bézout 等式两边同乘 ,得特解 ,它满足 。
从特解到最小非负解。 若
实现的两个细节。 其一,% 对负数返回非负余数,所以先做 % step 归一化即可,无需手动调整符号。
用样例验证推导。 样例
| A: |
B: |
是否相遇 | |
|---|---|---|---|
| 0 | 1 | 2 | 否 |
| 1 | 4 | 1 | 否 |
| 2 | 2 | 0 | 否 |
| 3 | 0 | 4 | 否 |
| 4 | 3 | 3 | ✅ 首次相遇 |
观察重点:每一行 A 与 B 的差恰好减少 Impossible 的来源。
代码
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:08
# update_at: 2026-09-30 23:08
import sys
IMPOSSIBLE = "Impossible" # gcd(y-x, L) 不整除差值时的答案
def exgcd(a: int, b: int) -> tuple[int, int, int]:
"""返回 (g, s, t),满足 a*s + b*t = g = gcd(a, b),g 取非负。"""
if b == 0:
return a, 1, 0
g, s, t = exgcd(b, a % b)
return g, t, s - a // b * t # 回代:a*x + b*y = g 的解由下层 (s,t) 推出
def solve() -> None:
x, y, m, n, L = map(int, sys.stdin.buffer.read().split())
# t 次跳跃后相遇 ⇔ x + m*t ≡ y + n*t (mod L) ⇔ (m-n)*t ≡ y-x (mod L)
a = (m - n) % L # 化成 0 ≤ a < L 的同余系数
c = (y - x) % L # 化成 0 ≤ c < L 的目标余数
g, s, _ = exgcd(a, L)
if c % g:
print(IMPOSSIBLE)
else:
# a*s ≡ g (mod L),两边乘 c/g 得特解,再除以 g/L 归一化周期
step = L // g
print(s * (c // g) % step)
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:22
* update_at: 2026-10-05 23:22
*/
// main.cpp:把相遇条件写成线性同余方程 (m-n)*t ≡ y-x (mod L),用 exgcd 求最小非负解。
#include <cstdio>
typedef long long ll;
// 扩展欧几里得:返回 gcd(a, b),并求出 x, y 使得 a*x + b*y = 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; // 回代:下层解 (x1, y1) 推出本层解
return g;
}
ll x, y, m, n, L; // 题目输入:起点、步长、环长
int main() {
scanf("%lld %lld %lld %lld %lld", &x, &y, &m, &n, &L);
// t 次跳跃后相遇 ⇔ x + m*t ≡ y + n*t (mod L) ⇔ (m-n)*t ≡ y-x (mod L)
ll a = (m - n) % L; // 系数可能是负数,先归一化到 [0, L)
if (a < 0) a += L;
ll c = (y - x) % L; // 目标余数同样归一化到 [0, L)
if (c < 0) c += L;
ll s, k;
ll g = exgcd(a, L, s, k); // a*s + L*k = g,即 a*s ≡ g (mod L)
if (c % g != 0) { // g 不整除 c 时同余方程无解
printf("Impossible\n");
return 0;
}
// 两边同乘 c/g 得特解 t0 = s * (c/g);全部解构成公差 L/g 的等差数列
ll step = L / g;
ll t = s % step * (c / g) % step; // 先取模防溢出,再归一化到最小非负解
if (t < 0) t += step;
printf("%lld\n", t);
return 0;
}代码要点与上文推导一一对应:
exgcd用递归实现 Bézout:边界时 ;回代时下层解 变换为 。 - 主流程先把
、 归一化到 ,再用 c % g判无解;有解时特解对周期 L // g取模,直接得到最小非负解。
复杂度
- 时间复杂度:exgcd 的递归深度与辗转相除相同,为
,其余步骤都是常数次大整数运算,总体 ,远优于朴素模拟的 。 - 空间复杂度:递归栈
;改成迭代可以降到 ,但递归写法与推导对应更直接。
总结
- 相遇条件
线性同余方程 ,是"循环追赶"类问题的标准转化。 - 解同余方程三步:exgcd 求
与 Bézout 系数 → 判有解 → 特解乘 后对周期 取模得最小非负解。 - 负系数、大数取模是本题两个易错点:先
% L化标准形,最后统一% step归一化,可以避免所有符号分类讨论。