跳 k 次后位置为 (x+kd) mod n,问题化为同余方程 kd ≡ y-x (mod n):gcd(d,n) 不整除差值即无解,约简后求模逆元得到最小非负解。
OJ: roj
题目 ID: 1638
难度:普及+/提高-
标签:数论扩展欧几里得同余方程python
创建: 2026-09-30 23:32
更新: 2026-10-07 13:50
形式化题目
给定长度为
求最小的非负整数 Impossible。
输入含
样例
| 0 | 1 | 2 | 3 | … | |
|---|---|---|---|---|---|
| 位置 | 0 | 2 | 1 | 0 | 周期 3 |
可见第 1 次到 1、2 一致。
正解
思路
设跳了
于是问题从“模拟跳跃”变成了“解一个同余方程”,答案就是 (1) 的最小非负整数解。
第一步:判断有没有解。 记
若不整除,大圣的落脚点是一串永远踩不到 Impossible。
第二步:约简到系数可逆。 当
此时
第三步:取最小非负解。 两边乘上逆元:
方程的全部整数解是
Python 中这一串推导可以合成为一个表达式:gcd(d, n) 给出 (y - x) % g 做整除判定,
pow(d // g, -1, 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:32
# update_at: 2026-09-30 23:32
import sys
from math import gcd
IMPOSSIBLE = "Impossible" # 无解时的输出,单独命名避免散落在分支里
def min_jumps(n: int, d: int, x: int, y: int) -> int | None:
"""连跳 d 步首次落到 y 的最少次数;永远到不了则返回 None。
跳 k 次后的位置是 (x + k*d) mod n,所以要求 k*d ≡ y-x (mod n)。
设 g = gcd(d, n):同余方程有解的充要条件是 g | (y-x);
否则两边同除以 g,得模 m = n/g 下唯一的同余类,
乘上 d/g 的模逆元再对 m 取模,就是最小的非负跳跃次数。
"""
g = gcd(d, n)
if (y - x) % g:
return None # 差值不是 g 的倍数,无整数解
m = n // g # 约简后的模数,也是解的周期
return (y - x) // g % m * pow(d // g, -1, m) % m # pow(a, -1, m) 即 a 的模逆元
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
out: list[str] = []
T = next(data) # 测试数据组数
for _ in range(T):
n, d, x, y = next(data), next(data), next(data), next(data)
jumps = min_jumps(n, d, x, y)
out.append(IMPOSSIBLE if jumps is None else str(jumps))
print('\n'.join(out))
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:56
* update_at: 2026-10-05 23:56
*/
// 题意:佛祖手掌是长为 n 的圆圈,大圣每次逆时针飞 d,问从 x 到 y 最少翻几个筋斗。
// 思路:跳 k 次后位置是 (x + k*d) mod n,解线性同余方程 k*d ≡ y-x (mod n)。
// 设 g = gcd(d, n):g 不整除 (y-x) 时无解;否则两边除以 g,在模 m = n/g 下
// 用扩展欧几里得求 (d/g) 的逆元,得到最小非负解 k0,即答案。
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
ll n, d, x, y; // 每组询问的手掌长度 n、每次飞行距离 d、起点 x、终点 y
// 辗转相除求最大公约数
ll mygcd(ll a, ll b) {
while (b != 0) {
ll r = a % b;
a = b;
b = r;
}
return a;
}
// 扩展欧几里得:求 gcd(a, b),并求出 a*x + b*y = gcd 的一组整数解
ll exgcd(ll a, ll b, ll &px, ll &py) {
if (b == 0) {
px = 1;
py = 0;
return a;
}
ll gx = 0, gy = 0;
ll g = exgcd(b, a % b, gx, gy); // 先解子问题 b*x + (a%b)*y = g
px = gy; // 回代:a%b = a - (a/b)*b
py = gx - (a / b) * gy;
return g;
}
// 解方程 k*d ≡ y-x (mod n),返回最小非负解;无解返回 -1
ll solve_query() {
ll g = mygcd(d, n);
ll diff = ((y - x) % n + n) % n; // 差值先调成非负,方便整除判定
if (diff % g != 0) {
return -1; // g 不整除差值,永远到不了 y
}
ll m = n / g; // 约简后的模数,也是解的周期
ll p = 0, q = 0;
exgcd(d / g, m, p, q); // 此时 gcd(d/g, m) = 1,逆元存在
ll inv = ((p % m) + m) % m; // 把逆元调成 [0, m) 内的最小非负剩余
ll k = (diff / g % m) * inv % m; // 乘上逆元取模,k0 就是最少跳跃次数
return k;
}
int main() {
int T;
if (scanf("%d", &T) != 1) {
return 0;
}
while (T--) {
scanf("%lld %lld %lld %lld", &n, &d, &x, &y);
ll ans = solve_query();
if (ans == -1) {
printf("Impossible\n");
} else {
printf("%lld\n", ans);
}
}
return 0;
}复杂度
- 时间复杂度:
。每组数据一次 gcd加一次模逆元,各为,其余为 算术。 - 空间复杂度:
额外空间(不含读入缓冲),与 无关。
总结
题目把「圆环上单向等步长行走」抽象成一个同余方程
图示解析
下图给出每组询问的判定与求解流程,重点看两条分支:不可达分支在
flowchart TD
Start[读入 n d x y] --> G[g = gcd d n]
G --> Div{g 整除 y - x ?}
Div -- 否 --> Imp[输出 Impossible]
Div -- 是 --> M[m = n / g]
M --> Inv["求 d/g 在模 m 下的逆元"]
Inv --> Sol["k0 = (y-x)/g * inv mod m"]
Sol --> Ans[输出 k0]
流程中唯一的“复杂”运算是模逆元;它对应 (2) 式两侧同乘逆元这一步。k0 一旦求出就是最终答案,无需再回代验证。