首页

首页 / 算法

同余方程怎么解:从判别式到可运行代码

一次同余方程 ax ≡ b (mod m) 有没有解、有几个解、怎么全部找出来。用扩展欧几里得求特解,再写中国剩余定理与一份 Python 实现。

同余方程怎么解:从判别式到可运行代码

一次同余方程看起来只是 axb(modm)ax \equiv b \pmod m,真正要回答的却有三问:有没有解、有几个解、怎么把它们全部找出来。下面按这条线索把原理、手算和代码放在一起。

同余在说什么

整数 aabb 对模 mm 同余,记作

ab(modm)a \equiv b \pmod m

意思是 mm 整除 aba-b,也就是两者除以 mm 余数相同。例如 175(mod12)17 \equiv 5 \pmod{12},因为 175=1217-5=12

同余可以像普通等式那样做加减乘,但不能随便约分。615(mod9)6 \equiv 15 \pmod 9 成立,两边同除以 33 得到 25(mod9)2 \equiv 5 \pmod 9,这就错了。正确做法是:约掉的因子必须和模数一起约。

一次同余方程

最常见的是一次同余方程

axb(modm)ax \equiv b \pmod m

其中 m>0m > 0,未知数 xx 是整数。它等价于:存在整数 kk,使得

axmy=bax - my = b

这是二元一次不定方程,也叫线性丢番图方程。于是整套理论可以落到一件事上:gcd(a,m)\gcd(a,m),再看它能不能整除 bb

d=gcd(a,m)d = \gcd(a,m)

  • dbd \nmid b,无解。
  • dbd \mid b,恰有 dd 个模 mm 互不同余的解。
  • 先求出一个特解 x0x_0,通解就是
xx0+mdt(modm),t=0,1,,d1x \equiv x_0 + \frac{m}{d}\, t \pmod m,\qquad t = 0,1,\dots,d-1

也可以写成对更小的模 md\frac{m}{d} 只有一个解:

xx0(modmd)x \equiv x_0 \pmod{\frac{m}{d}}

为什么是这个判别

axmy=bax - my = b 的左边一定是 dd 的倍数,因为 dd 同时整除 aamm。所以右边 bb 也必须是 dd 的倍数,否则无解。

反过来,裴蜀定理保证:存在整数 x,yx,y 使 ax+my=dax + my = d。两边乘 b/db/d,就得到 bb 的一组表示,于是原方程有解。

解的个数来自周期。若 x0x_0 是特解,则

x0+mdtx_0 + \frac{m}{d}t

也都是解。这些解模 mm 恰好分成 dd 类,对应 t=0,1,,d1t = 0,1,\dots,d-1

扩展欧几里得:把特解算出来

普通欧几里得算法只给出 dd。扩展欧几里得在辗转相除的同时,把

d=ax+myd = ax + my

里的系数也记下来。

迭代写法最适合写代码。维护两组系数,使每一步的余数都能写成 aamm 的整数组合:

def egcd(a: int, b: int) -> tuple[int, int, int]:
    """返回 (d, x, y),满足 d = gcd(a, b) = a*x + b*y。"""
    old_r, r = a, b
    old_s, s = 1, 0
    old_t, t = 0, 1
    while r:
        q = old_r // r
        old_r, r = r, old_r - q * r
        old_s, s = s, old_s - q * s
        old_t, t = t, old_t - q * t
    return old_r, old_s, old_t

得到 d,x,yd,x,y 后,xx 就是 axd(modb)ax \equiv d \pmod b 的一个解。原方程若 dbd \mid b,特解就是

x0=xbdx_0 = x \cdot \frac{b}{d}

再把 x0x_0 收进 [0,m)[0, m) 即可。

手算例子

例一:有唯一解

3x4(mod7)3x \equiv 4 \pmod 7

gcd(3,7)=1\gcd(3,7)=1,能整除 44,所以模 77 恰有一解。

用扩展欧几里得:

7=23+13=31+0\begin{aligned} 7 &= 2\cdot 3 + 1 \\ 3 &= 3\cdot 1 + 0 \end{aligned}

回代:1=7231 = 7 - 2\cdot 3,故 3(2)1(mod7)3\cdot(-2) \equiv 1 \pmod 7。两边乘 44

3(8)4(mod7)    x86(mod7)3\cdot(-8) \equiv 4 \pmod 7 \implies x \equiv -8 \equiv 6 \pmod 7

验算:36=184(mod7)3\cdot 6 = 18 \equiv 4 \pmod 7

例二:有多个解

6x9(mod15)6x \equiv 9 \pmod{15}

d=gcd(6,15)=3d=\gcd(6,15)=3,且 393 \mid 9,所以有 33 个解。先把方程除以 33

2x3(mod5)2x \equiv 3 \pmod 5

2255 的逆元是 33,因为 23=612\cdot 3=6\equiv 1。于是

x94(mod5)x \equiv 9 \equiv 4 \pmod 5

特解 x0=4x_0=4,通解

x=4+5t,t=0,1,2x = 4 + 5t,\qquad t=0,1,2

也就是模 1515 的三个解:4,9,144,9,14

验算:64=2496\cdot 4=24\equiv 969=5496\cdot 9=54\equiv 9614=849(mod15)6\cdot 14=84\equiv 9 \pmod{15}

例三:无解

4x2(mod6)4x \equiv 2 \pmod 6gcd(4,6)=2\gcd(4,6)=2,但 22 不能整除……等等,22 能整除 22,所以有解。改成 4x3(mod6)4x \equiv 3 \pmod 6gcd=2\gcd=2232 \nmid 3,无解。直观理解是:偶数乘任何整数再模 66,余数只能是偶数,到不了 33

模逆元是特例

gcd(a,m)=1\gcd(a,m)=1 时,ax1(modm)ax\equiv 1 \pmod m 有唯一解,这个解叫 aamm 的逆元,记作 a1a^{-1}。于是

axb(modm)    xa1b(modm)ax \equiv b \pmod m \iff x \equiv a^{-1} b \pmod m

费马小定理给素数模一个快算法:若 pp 是素数且 pap \nmid a,则

a1ap2(modp)a^{-1} \equiv a^{p-2} \pmod p

一般模数仍用扩展欧几里得更稳妥。

def modinv(a: int, m: int) -> int:
    d, x, _ = egcd(a, m)
    if d != 1:
        raise ValueError(f"{a}{m} 不互素,没有逆元")
    return x % m

同余方程组:中国剩余定理

一组两两互素的模

{xa1(modm1)xa2(modm2)xak(modmk)\begin{cases} x \equiv a_1 \pmod{m_1} \\ x \equiv a_2 \pmod{m_2} \\ \vdots \\ x \equiv a_k \pmod{m_k} \end{cases}

在模 M=m1m2mkM=m_1 m_2 \cdots m_k 下恰有一解。构造法是:

Mi=Mmi,tiMi1(modmi),x=i=1kaiMitiM_i = \frac{M}{m_i},\qquad t_i \equiv M_i^{-1} \pmod{m_i},\qquad x = \sum_{i=1}^{k} a_i M_i t_i

最后取 xmodMx \bmod M

若模数不两两互素,仍可能有解,条件更严:对任意 i,ji,j

aiaj(modgcd(mi,mj))a_i \equiv a_j \pmod{\gcd(m_i,m_j)}

这时可以用两两合并:先解前两个,得到一个新的同余,再和下一个合并。

例四:韩信点兵

{x2(mod3)x3(mod5)x2(mod7)\begin{cases} x \equiv 2 \pmod 3 \\ x \equiv 3 \pmod 5 \\ x \equiv 2 \pmod 7 \end{cases}

M=105M=105M1=35M_1=35M2=21M_2=21M3=15M_3=15

  • 352(mod3)35 \equiv 2 \pmod 3,逆元 22,因为 22=412\cdot 2=4\equiv 1
  • 211(mod5)21 \equiv 1 \pmod 5,逆元 11
  • 151(mod7)15 \equiv 1 \pmod 7,逆元 11
x=2352+3211+2151=140+63+30=23323(mod105)x = 2\cdot 35\cdot 2 + 3\cdot 21\cdot 1 + 2\cdot 15\cdot 1 = 140+63+30=233 \equiv 23 \pmod{105}

2323 除以 3322,除以 5533,除以 7722

一份可直接用的实现

下面把「单方程」和「方程组」放在一起。单方程返回模 mm 下全部互异解;方程组用两两合并,不要求模数互素。

from math import gcd
 
 
def egcd(a: int, b: int) -> tuple[int, int, int]:
    old_r, r = a, b
    old_s, s = 1, 0
    old_t, t = 0, 1
    while r:
        q = old_r // r
        old_r, r = r, old_r - q * r
        old_s, s = s, old_s - q * s
        old_t, t = t, old_t - q * t
    return old_r, old_s, old_t
 
 
def solve_linear(a: int, b: int, m: int) -> list[int]:
    """解 a x ≡ b (mod m),返回 [0, m) 内全部解;无解返回空列表。"""
    if m <= 0:
        raise ValueError("模数必须为正")
    a, b = a % m, b % m
    d, x, _ = egcd(a, m)
    if b % d:
        return []
    x0 = (x * (b // d)) % (m // d)
    step = m // d
    return sorted((x0 + step * t) % m for t in range(d))
 
 
def merge(a1: int, m1: int, a2: int, m2: int) -> tuple[int, int] | None:
    """合并 x ≡ a1 (mod m1) 与 x ≡ a2 (mod m2)。"""
    d = gcd(m1, m2)
    if (a2 - a1) % d:
        return None
    # 解 m1 * t ≡ a2 - a1 (mod m2)
    t = solve_linear(m1, a2 - a1, m2)
    if not t:
        return None
    lcm = m1 // d * m2
    return (a1 + m1 * t[0]) % lcm, lcm
 
 
def solve_system(congruences: list[tuple[int, int]]) -> tuple[int, int] | None:
    """congruences 为 [(a, m), ...],表示 x ≡ a (mod m)。
    有解返回 (x0, modulus),无解返回 None。"""
    x, mod = 0, 1
    for a, m in congruences:
        merged = merge(x, mod, a, m)
        if merged is None:
            return None
        x, mod = merged
    return x, mod
 
 
if __name__ == "__main__":
    print(solve_linear(3, 4, 7))          # [6]
    print(solve_linear(6, 9, 15))         # [4, 9, 14]
    print(solve_linear(4, 3, 6))          # []
    print(solve_system([(2, 3), (3, 5), (2, 7)]))  # (23, 105)

跑一下就能对上前面的手算。solve_linear 的时间是欧几里得的 O(logm)O(\log m),再加 O(d)O(d) 枚举全部解;方程组是依次合并,模数变大时注意用 Python 整数即可,不必担心溢出。

写代码时容易踩的坑

先取模再求 gcd\gcdaa 可能是负数,Python 的 % 会收成非负,扩展欧几里得也能处理负数,但先规范化更不容易错。

特解要乘 b/db/d,不是乘 bb。漏掉这一步,方程就解成了 axdax\equiv d 而不是 axbax\equiv b

通解的步长是 m/dm/d,不是 dd6x9(mod15)6x\equiv 9\pmod{15} 的相邻解相差 55,不是 33

合并方程组时,新模是 lcm(m1,m2)=m1m2/gcd\mathrm{lcm}(m_1,m_2)=m_1 m_2 / \gcd,不是随便乘。模数不互素时,这一步决定解是否唯一。

若只要任意一个解,不必枚举 dd 个,返回 x0x_0 即可。竞赛里常见「输出任意解或 1-1」。

和更高次方程的边界

二次同余 x2n(modp)x^2 \equiv n \pmod p 在素数模上可以用 Cipolla 或 Tonelli–Shanks,那是另一套二次剩余的语言。更高次则进入原根、指数和离散对数。一次同余是这些算法的底座:求逆、CRT 拆模、把大模拆成素因子幂,几乎都会先回到 axbax\equiv b

所以不必把一次同余看成小学奥数。它是数论算法里最常被调用的那一层。把存在性、特解和通解三件事分开,手算和代码就会对得上。