「一本通 6.4 例 1」青蛙的约会

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

形式化题目

环长为 LL 的圆周上有两个动点,初始位置 xx、yy(x≠yx \neq y),每一步分别前进 mm、nn。求最小的非负整数 tt,使得

x+mt≡y+nt(modL)x + mt \equiv y + nt \pmod{L}

若这样的 tt 不存在,输出 Impossible。数据范围:0⩽x,y<2×1090 \leqslant x, y < 2 \times 10^9,0<m,n,L<2×1090 < m, n, L < 2 \times 10^9。

正解

思路

朴素做法与瓶颈。 最直接的想法是模拟:每一步让两只青蛙各前进一次,检查位置是否相同。单次跳跃后 A 领先 B 的距离减少 m−nm - n(模 LL 意义下),所以相遇时间 tt 最大可能达到 LL,而 L<2×109L < 2 \times 10^9——模拟一步一查是 O(L)O(L),必然超时。必须从"一步一跳"升级到"解方程"。

把相遇写成同余方程。 相遇条件是两者位置模 LL 相等:

x+mt≡y+nt(modL)x + mt \equiv y + nt \pmod{L}

移项,未知数只有 tt:

(m−n) t≡y−x(modL)(m - n)\,t \equiv y - x \pmod{L}

这是一个标准的线性同余方程 at≡c(modL)at \equiv c \pmod{L},其中 a=m−na = m - n,c=y−xc = y - x。数据范围在 2×1092 \times 10^9 级别,Python 的整数不受影响,关键是求出 tt 的算法要降到 O(log⁡L)O(\log L)——这正是扩展欧几里得(exgcd)的用武之地。

用 exgcd 解同余方程。 设 g=gcd⁡(a,L)g = \gcd(a, L)。由 Bézout 定理,exgcd 能求出整数 ss 使 a⋅s+L⋅(某整数)=ga \cdot s + L \cdot (\text{某整数}) = g,即

a⋅s≡g(modL)a \cdot s \equiv g \pmod{L}
  • 有解判定:同余方程 at≡c(modL)at \equiv c \pmod L 有解当且仅当 g∣cg \mid c。直观理解:at−cat - c 必须是 LL 的倍数,而 gg 同时整除 aa 与 LL,所以 gg 必须整除 cc。若 g∤cg \nmid c,输出 Impossible。
  • 求特解:当 g∣cg \mid c 时,把 Bézout 等式两边同乘 c/gc / g,得特解 t0=s⋅cgt_0 = s \cdot \dfrac{c}{g},它满足 a t0≡c(modL)a\,t_0 \equiv c \pmod L。

从特解到最小非负解。 若 t0t_0 是解,则 t0+k⋅Lgt_0 + k \cdot \dfrac{L}{g} 也都是解(因为 a⋅Lga \cdot \dfrac{L}{g} 恰好是 LL 的 a/ga/g 倍,模 LL 为 00)。所以全部解构成公差为 Lg\dfrac{L}{g} 的等差数列,最小非负解就是

t=t0 mod Lgt = t_0 \bmod \frac{L}{g}

实现的两个细节。 其一,a=m−na = m - n 可能是负数(两只青蛙谁快谁慢不确定),Python 的 % 对负数返回非负余数,所以先做 a←(m−n) mod La \leftarrow (m-n) \bmod L、c←(y−x) mod Lc \leftarrow (y-x) \bmod L,把方程化成 0⩽a<L0 \leqslant a < L、0⩽c<L0 \leqslant c < L 的标准形式,后续取模方向不会出错。其二,exgcd 递归返回的 ss 符号不定,最后一步统一 % step 归一化即可,无需手动调整符号。

用样例验证推导。 样例 x=1,y=2,m=3,n=4,L=5x=1, y=2, m=3, n=4, L=5:方程为 (−1)t≡1(mod5)(-1)t \equiv 1 \pmod 5,化成标准形 a=4, c=1a = 4,\ c = 1,g=gcd⁡(4,5)=1g = \gcd(4,5) = 1,特解 s=4s = 4(4×4=16≡14 \times 4 = 16 \equiv 1),周期 L/g=5L/g = 5,答案 4 mod 5=44 \bmod 5 = 4。逐跳模拟也能对上:第 tt 跳后 A 在 (1+3t) mod 5(1+3t) \bmod 5、B 在 (2+4t) mod 5(2+4t) \bmod 5,逐项列出:

tt A:(1+3t) mod 5(1+3t) \bmod 5 B:(2+4t) mod 5(2+4t) \bmod 5 是否相遇
0 1 2 否
1 4 1 否
2 2 0 否
3 0 4 否
4 3 3 ✅ 首次相遇

观察重点:每一行 A 与 B 的差恰好减少 11(即 m−nm-n 的模 LL 效果),差值 4,3,2,1,04, 3, 2, 1, 0 走完一个周期才追上——这正是方程 4t≡1(mod5)4t \equiv 1 \pmod 5 的几何含义。当 g>1g > 1 时差值每次减少 gg 的倍数,差值永远落在 gg 的剩余类里,g∤cg \nmid c 时就永远追不上,这解释了 Impossible 的来源。

代码

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: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++ 版(同一算法):

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: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:边界 b=0b=0 时 g=a, s=1g=a,\ s=1;回代时下层解 (s,t)(s, t) 变换为 (t, s−a//b×t)(t,\ s - a // b \times t)。
  • 主流程先把 aa、cc 归一化到 [0,L)[0, L),再用 c % g 判无解;有解时特解 s⋅(c/g)s \cdot (c/g) 对周期 L // g 取模,直接得到最小非负解。

复杂度

  • 时间复杂度:exgcd 的递归深度与辗转相除相同,为 O(log⁡min⁡(a,L))O(\log \min(a, L)),其余步骤都是常数次大整数运算,总体 O(log⁡L)O(\log L),远优于朴素模拟的 O(L)O(L)。
  • 空间复杂度:递归栈 O(log⁡L)O(\log L);改成迭代可以降到 O(1)O(1),但递归写法与推导对应更直接。

总结

  • 相遇条件 ⇔\Leftrightarrow 线性同余方程 (m−n)t≡y−x(modL)(m-n)t \equiv y-x \pmod L,是"循环追赶"类问题的标准转化。
  • 解同余方程三步:exgcd 求 gg 与 Bézout 系数 → g∣cg \mid c 判有解 → 特解乘 c/gc/g 后对周期 L/gL/g 取模得最小非负解。
  • 负系数、大数取模是本题两个易错点:先 % L 化标准形,最后统一 % step 归一化,可以避免所有符号分类讨论。