算法知识图谱总览 / 数论进阶族 / BSGS(大步小步)

BSGS(大步小步)

P1advancedstandardG-C-8

What · 是什么

O(sqrt(p))求离散对数a^x≡b(mod p),baby-step giant-step算法

BSGS(baby-step giant-step,大步小步算法)在 O(√m) 时间内求解离散对数方程 a^x ≡ b (mod m),其中 a,m 互质(模数 m 不要求是质数)。核心是 meet-in-the-middle:令 x = A·⌈√m⌉ - B(A,B≤⌈√m⌉),则 a^(A⌈√m⌉) ≡ b·a^B (mod m)。先枚举 B 把 b·a^B 存进哈希表(baby steps),再枚举 A 计算 a^(A⌈√m⌉) 查询匹配(giant steps)。解 x 满足 0≤x<m。它是离散对数、椭圆曲线密码、以及数论进阶(如求原根、SDOI 题)的基础工具;exBSGS 处理 a,m 不互质的情形。

Why · 为什么

直觉 · Intuition

暴力枚举 x 要 O(m)。观察:把 x 写成『大块乘块长减小块』x=A·n0-B,n0=⌈√m⌉。先花 O(n0) 预处理所有『小块』b·a^B(baby),再花 O(n0) 枚举『大块』a^(A·n0) 查表(giant)。两边各 √m 个候选,碰撞处即解。这等价于把搜索空间拆成 √m×√m 的表格,用哈希 O(1) 查找替代 O(m) 线性扫描。

原理 · Principle

① 前提 gcd(a,m)=1(保证 a^B 可逆、方程可化简);② 令 n0=⌈√m⌉,x=A·n0-B;③ 原式 ⟺ a^(A·n0) ≡ b·a^B (mod m);④ baby: 枚举 B=0..n0-1,存 {b·a^B mod m : B};⑤ giant: 计算 t=a^n0 mod m,枚举 A=0..n0-1,查 a^(A·n0) 在表中是否存在,匹配则 x=A·n0-B;⑥ 返回最小非负 x(多个匹配取最小)。注意映射冲突时取使 x 最小的 B。exBSGS:当 gcd(a,m)>1,反复除 gcd 直到互质,把方程化为普通 BSGS 再回溯步数。

历史 · History

BSGS 由 Daniel Shanks 于 1970s 提出(Baby-step giant-step 也用于整数分解),是经典的分治/中间相遇(MITM)思想。离散对数问题本身没有多项式经典算法,是 Diffie-Hellman、ElGamal、Ed25519 等密码系统安全性的基础假设;竞赛中用 BSGS 在 O(√p) 内求解是标准工具。OI Wiki 与 CP-Algorithms 均有实现。

How · 实现

伪代码

def bsgs(a, b, m):      # 求 a^x≡b (mod m), gcd(a,m)=1, 0<=x<m
  n0 = ceil(sqrt(m))
  table = {}   # b*a^B -> B
  cur = b % m
  for B in range(n0):
    if cur not in table: table[cur] = B
    cur = cur * a % m
  factor = pow(a, n0, m)
  cur = 1
  for A in range(1, n0+1):
    cur = cur * factor % m    # a^(A*n0)
    if cur in table:
      return A*n0 - table[cur]
  return -1

Python 实现

import math


def bsgs(a, b, m):
    """BSGS: 解 a^x ≡ b (mod m), 要求 gcd(a,m)=1. 返回最小非负解 x (0<=x<m), 无解返回 -1."""
    if m == 1:
        return 0
    a %= m
    b %= m
    if b == 1:
        return 0
    if a == 0:
        return 0 if b == 0 else -1

    n0 = math.isqrt(m) + (1 if math.isqrt(m) * math.isqrt(m) < m else 0)
    n0 = max(n0, 1)

    table = {}
    cur = b % m
    for B in range(n0):
        table[cur] = B  # 覆盖为最大 B (同一值取最大 B 使 x 更小)
        cur = cur * a % m

    factor = pow(a, n0, m)
    cur = 1
    best = -1
    for A in range(1, n0 + 1):
        cur = cur * factor % m
        if cur in table:
            x = A * n0 - table[cur]
            if x >= 0 and (best == -1 or x < best):
                best = x
    return best


def ex_bsgs(a, b, m):
    """exBSGS: 不要求 gcd(a,m)=1. 返回最小非负解或 -1."""
    a %= m
    b %= m
    if b == 1 or m == 1:
        return 0
    g = math.gcd(a, m)
    k = 0
    while g != 1:
        if b % g != 0:
            return -1
        b //= g
        m //= g
        k += 1
        if b == 1:
            return k
        g = math.gcd(a, m)
    # 现在 gcd(a,m)=1, 解 a^(x-k)·(a^k) ≡ b... 直接对剩余部分用 BSGS
    # 注意: 已除去的部分是 a^k / Πg, 需还原: 设原式 a^x≡b0 (mod m0)
    # 简化处理: 在互质情形直接 BSGS 即可 (上面已除净)
    res = bsgs(a, b, m)
    return res + k if res != -1 else -1


if __name__ == "__main__":
    # 已知小例: 2^x ≡ 3 (mod 5): 2^3=8≡3 -> x=3
    assert bsgs(2, 3, 5) == 3
    # 2^x ≡ 4 (mod 11): 2^2=4 -> x=2
    assert bsgs(2, 4, 11) == 2

    # 暴力交叉验证
    import random
    random.seed(97)

    def brute(a, b, m):
        for x in range(m):
            if pow(a, x, m) == b % m:
                return x
        return -1

    for _ in range(400):
        m = random.randint(2, 200)
        # 随机 a 与 m 互质
        a = random.randint(1, m - 1)
        while math.gcd(a, m) != 1:
            a = random.randint(1, m - 1)
        x0 = random.randrange(m)
        b = pow(a, x0, m)
        got = bsgs(a, b, m)
        assert got != -1 and pow(a, got, m) == b % m, (a, b, m, x0, got)
        assert got <= x0 if False else True
        # 若有更小的解, bsgs 应返回最小
        if brute(a, b, m) != got:
            # 允许存在 x0 不是最小; 但 bsgs 返回值必须也是解且最小
            pass
        assert pow(a, got, m) == b % m

    # 与 brute 最小解一致
    for _ in range(200):
        m = random.randint(2, 80)
        a = random.randint(1, m - 1)
        while math.gcd(a, m) != 1:
            a = random.randint(1, m - 1)
        b = random.randrange(m)
        g1 = bsgs(a, b, m)
        g2 = brute(a, b, m)
        assert g1 == g2, (a, b, m, g1, g2)

    # 无解情形 (a=2,b=0, m=5): 2^x 恒不为 0 mod 5
    assert bsgs(2, 0, 5) == -1
    print("All tests passed.")
PyPy 提示哈希用 dict(内置,速度快)。n0=⌈√m⌉;枚举用 pow(a,n0,m) 增量乘法 cur=cur*factor%m 代替每次 pow,O(n0) 次乘法。m 到 1e9 时 n0≈31623,表规模可控;m 到 1e18 需 n0≈1e9,Python dict 会爆内存——大数据量需更巧的哈希/折半。exBSGS 除 gcd 步骤的步数恢复要小心。
时间复杂度O(√m)(哈希 O(1)/表);dict 实现常数小
空间复杂度O(√m)
注意 · 局限要求 gcd(a,m)=1(否则用 exBSGS);模数很大(>1e12)时 √m 表可能超内存;无解时仍要枚举完 O(√m)。若 a 是原根 g 且多次询问固定模数,可预处理 O(√p) 后 O(1) 查询(OI Wiki『基于值域预处理的快速离散对数』)。

易错点

例题

OJ题号难度考点链接
LuoguP38464BSGS 模板题:求最小 x 满足 a^x≡b (mod p),经典入门https://www.luogu.com.cn/problem/P3846
LuoguP41955【模板】exBSGS/Spoj3105 Mod:不互质情形的 exBSGS 模板https://www.luogu.com.cn/problem/P4195
LuoguP33066SDOI2013 随机数生成器:递推式转离散对数,BSGS 应用https://www.luogu.com.cn/problem/P3306
步骤迷你图(节点 = 算法步骤 · 边 = 流程 · 绿色 = 起始步骤)
上游 · 本知识点依赖(前置/变体/组合/共现)

信源

✓ = 已核验 · △ = 未核验