exBSGS 算法

exBSGS 算法

一、问题:离散对数

给定整数 \(a,b,p\),求最小非负整数 \(x\) 使

\a\^x \\equiv b \\pmod p \\

这就是离散对数问题 (Discrete Logarithm),记作 \(x=\log_a b\pmod p\)。

朴素做法的瓶颈

由欧拉定理,当 \(\gcd(a,p)=1\) 时序列 \(a^x\bmod p\) 的周期不超过 \(\varphi(p)\le p\),枚举 \(x=0,1,\dots,p-1\) 即可,复杂度 \(O(p)\)。当 \(p\sim 10^9\) 时不可接受。

两个层次

算法 适用条件 时间复杂度
BSGS \(\gcd(a,p)=1\) \(O(\sqrt p)\)
exBSGS 无限制 \(O(\sqrt p)\)

exBSGS 的核心:当 \(\gcd(a,p)\ne 1\) 时,反复约去 \(\gcd(a,p)\),把问题归约为 \(\gcd(a,p')=1\) 的情形,再交给 BSGS。


二、BSGS 回顾(扩展的基础)

设 \(m=\lceil\sqrt p\rceil\)。把指数写成

\x = im - j,\\qquad 0\\le j \< m,\\quad i\\ge 1 \\

代入 \(a^{im-j}\equiv b\pmod p\)。因为 \(\gcd(a,p)=1\),\(a^j\) 可逆,两边乘 \(a^j\):

\(a\^m)\^i \\equiv b\\cdot a\^j \\pmod p \\

左端只与 \(i\) 有关,右端只与 \(j\) 有关,分两步:

  • Baby Step(小步) :预计算 \(b\cdot a^j\bmod p\;(j=0,\dots,m-1)\),以值为键、\(j\) 为值存入哈希表(覆盖写入 ,保留最大 \(j\))。
  • Giant Step(大步) :枚举 \(i=1,\dots,m\),计算 \((a^m)^i\bmod p\) 查表;命中即 \(x=im-j\)。

复杂度 :时间、空间均 \(O(\sqrt p)\)。

关键依赖 :\(\gcd(a,p)=1\Rightarrow a\) 可逆,才能把 \(a^{-j}\) 移到右边变 \(a^j\)。一旦 \(\gcd(a,p)\ne 1\),BSGS 直接失效。


三、为什么 \(\gcd(a,p)\ne 1\) 时 BSGS 失效

设 \(d=\gcd(a,p)>1\)。此时 \(a\) 模 \(p\) 不可逆 ,\(a^{-j}\) 不存在。

更本质地:当 \(\gcd(a,p)>1\) 时,\(a\) 的幂序列 \(\{a^0,a^1,\dots\}\bmod p\) 不构成 模 \(p\) 的既约剩余系中的循环群------从某项起所有项都被 \(d\) 整除,无法覆盖全部余数。

直觉 :把 \(a\) 与 \(p\) 的公共因子"剥离"出去,直到剩余模数与 \(a\) 互素,再交给 BSGS。


四、核心思想:消因子法(约简)

4.1 基本观察

\a\^x\\equiv b\\pmod p\\iff p\\mid(a\^x-b) \\

设 \(d=\gcd(a,p)\)。因 \(d\mid a\) 且 \(d\mid p\):

  • \(d\mid a\Rightarrow d\mid a^x\;(x\ge 1)\)
  • 故 \(d\mid b\) 是 \(x\ge 1\) 时有解的必要条件

例外 :\(x=0\) 时 \(a^0=1\),无需 \(d\mid b\),故 \(x=0\) 须单独检查。

4.2 消去一步

当 \(d\mid b\) 时,原方程两边同除以 \(d\):

\\\frac{a}{d}\\cdot a\^{x-1}\\equiv \\frac{b}{d}\\pmod{\\frac pd} \\

令 \(a'=a/d,\;b'=b/d,\;p'=p/d\),得 \(a'\cdot a^{x-1}\equiv b'\pmod{p'}\)。左侧多出一个系数 \(a'\)。若 \(\gcd(a,p')=1\),即可把 \(a'\) 求逆移到右边化为标准 BSGS;否则对 \(p'\) 继续消因子。

4.3 不变量(clean 表述)

维护变量,初值 \(A=1,\;B=b,\;P=p,\;k=0\)。循环不变量:原方程等价于

\\\boxed{A\\cdot a\^{x-k}\\equiv B\\pmod P} \\

其中 \(A\) 是历次 \(a/d_i\) 之积(对当前模数取模),\(B=b/\!\prod d_i\),\(P=p/\!\prod d_i\)。每消去一步,\(k\) 加 1(相当于"消耗" \(a\) 的一个因子、把指数降 1、同时缩减模数)。


五、逐步约简规则

设当前状态为 \(A\cdot a^{x-k}\equiv B\pmod P\),令 \(d=\gcd(a,P)\)。

  1. 检查 \(x=k\) (每轮开头做,天然覆盖 \(x=0\)):若 \(A\equiv B\pmod P\),则 \(a^k\equiv b\pmod p\),直接返回 \(k\)。

  2. \(d=1\),约简结束 :此时 \(\gcd(a,P)=1\),且可证 \(\gcd(A,P)=1\)(\(A\) 可逆),转入 BSGS。

  3. \(d>1\) 且 \(d\nmid B\),无解 :对 \(x>k\),\(a^{x-k}\) 被 \(d\) 整除 \(\Rightarrow Aa^{x-k}\) 被 \(d\) 整除,但 \(B\) 不被 \(d\) 整除,矛盾。整个方程无解。

  4. \(d>1\) 且 \(d\mid B\),约简 :两边同除以 \(d\)

    \\\frac{A\\cdot a}{d}\\cdot a\^{x-k-1}\\equiv\\frac{B}{d}\\pmod{\\frac Pd} \\

    更新 \(A\leftarrow A\cdot\frac ad\bmod\frac Pd,\;B\leftarrow B/d,\;P\leftarrow P/d,\;k\leftarrow k+1\)。

  5. 回到第 1 步继续。

为何第 1 步放在开头 :消去只能排除 \(x>k\) 无解;而 \(x=k\)(\(a^{x-k}=a^0=1\))可能在中间某步就满足方程。开头统一检查 \(A\equiv B\pmod P\),即可逐一覆盖 \(x=0,1,2,\dots\),保证最小解不漏。


六、正确性证明

6.1 \(\gcd(A,P)=1\) 的证明

命题 :约简结束时 \(\gcd(a,P)=1\Rightarrow\gcd(A,P)=1\)。

证明 :\(A=\prod(a/d_i)\),每个因子整除 \(a\),故 \(A\mid a^k\)。由 \(\gcd(a,P)=1\) 得 \(\gcd(a^k,P)=1\),进而 \(\gcd(A,P)=1\)。\(\square\)

因此 \(A\) 模 \(P\) 可逆,归约到 BSGS 合法。

6.2 等价性证明

命题 :\(A\cdot a^{x-k}\equiv B\pmod P\) 与 \(a^x\equiv b\pmod p\) 等价。

证明 :代入 \(A=a^k/D,\;B=b/D,\;P=p/D\)(\(D=\prod d_i\)):

\\\frac{a\^k}{D}\\cdot a\^{x-k}\\equiv\\frac bD\\pmod{\\frac pD} \\;\\Longrightarrow\\;\\frac{a\^x}{D}\\equiv\\frac bD\\pmod{\\frac pD} \\

\\\frac pD\\mid\\frac{a\^x-b}{D}\\iff p\\mid(a\^x-b)\\iff a\^x\\equiv b\\pmod p \\

最后一步用到 \(D\mid(a^x-b)\)(因 \(D\mid a^k\) 且 \(D\mid b\),而 \(a^x=a^k a^{x-k}\))。\(\square\)

6.3 最小性的保证

  1. \(x=0,1,\dots,k\) 在循环开头逐一被检查;
  2. \(x>k\) 的最小解由 BSGS 返回最小非负 \(t=x-k\) 保证,故 \(x=k+t\) 最小;
  3. 二者取并集得全局最小。

七、算法流程与 AC 代码

7.1 伪代码

复制代码
exBSGS(a, b, p):
    a ← a mod p;  b ← b mod p
    if p == 1: return 0            // 模 1 下恒为 0,a^0=1 即解
    A ← 1;  B ← b;  P ← p;  k ← 0
    while true:
        if A mod P == B mod P: return k     // 检查 x = k(含 x=0)
        d ← gcd(a, P)
        if d == 1: break                    // 已归约到 BSGS
        if B mod d != 0: return -1          // x > k 无解
        A ← A * (a/d) mod (P/d)
        B ← B / d
        P ← P / d
        k ← k + 1
    t ← BSGS(a, B * inv(A, P) mod P, P)     // gcd(a,P)=gcd(A,P)=1
    return t == -1 ? -1 : k + t

7.2 完整 C++ 实现(P4195 可过)

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll gcd(ll a, ll b) {
    while (b) { a %= b; swap(a, b); }
    return a;
}

// 安全乘法:用 __int128 防 1e9*1e9 溢出
ll mulmod(ll a, ll b, ll mod) {
    return (ll)((__int128)a * b % mod);
}

ll powmod(ll a, ll b, ll mod) {
    ll res = 1 % mod; a %= mod;
    while (b) {
        if (b & 1) res = mulmod(res, a, mod);
        a = mulmod(a, a, mod);
        b >>= 1;
    }
    return res;
}

// 扩展欧几里得求逆元(要求 gcd(a,p)=1)
void exgcd(ll a, ll b, ll &x, ll &y) {
    if (!b) { x = 1; y = 0; return; }
    exgcd(b, a % b, y, x);
    y -= a / b * x;
}
ll inv_mod(ll a, ll p) {
    ll x, y; exgcd(a, p, x, y);
    return (x % p + p) % p;
}

// 自定义哈希,防止 unordered_map 被卡
struct custom_hash {
    size_t operator()(ll x) const {
        x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ULL;
        x = (x ^ (x >> 27)) * 0x94d049bb133111ebULL;
        x = x ^ (x >> 31);
        return (size_t)x;
    }
};

// BSGS:a^x ≡ b (mod p),要求 gcd(a,p)=1,返回最小非负 x,无解 -1
ll bsgs(ll a, ll b, ll p) {
    a %= p; b %= p;
    if (p == 1) return 0;
    if (b == 1 % p) return 0;          // a^0 = 1 ≡ b

    // m = ceil(sqrt(p)),用整数法避免浮点误差
    ll m = (ll)sqrtl((long double)p);
    while (m * m < p) m++;

    unordered_map<ll, ll, custom_hash> baby;
    baby.reserve(m * 2 + 1);
    ll cur = b;
    for (ll j = 0; j < m; j++) {
        baby[cur] = j;                 // 覆盖写入:保留最大 j
        cur = mulmod(cur, a, p);
    }

    ll am = powmod(a, m, p);
    ll giant = am;
    for (ll i = 1; i <= m; i++) {
        auto it = baby.find(giant);
        if (it != baby.end()) return i * m - it->second;
        giant = mulmod(giant, am, p);
    }
    return -1;
}

// exBSGS:a^x ≡ b (mod p),无 gcd 限制,返回最小自然数 x,无解 -1
ll exbsgs(ll a, ll b, ll p) {
    a %= p; b %= p;
    if (p == 1) return 0;              // 任何数 mod 1 = 0,a^0=1 即解

    ll A = 1 % p, B = b, P = p, k = 0;
    while (true) {
        if (A % P == B % P) return k;  // 检查 x = k(含 x = 0)
        ll d = gcd(a, P);
        if (d == 1) break;             // 归约结束
        if (B % d != 0) return -1;     // x > k 无解

        A = mulmod(A, a / d, P / d);
        B = B / d;
        P = P / d;
        k++;
    }
    // 此时 gcd(a,P)=1 且 gcd(A,P)=1
    ll target = mulmod(B % P, inv_mod(A % P, P), P);
    ll t = bsgs(a, target, P);
    return t == -1 ? -1 : k + t;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    ll a, p, b;
    while (cin >> a >> p >> b && (a || p || b)) {
        ll ans = exbsgs(a, b, p);
        if (ans == -1) cout << "No Solution\n";
        else cout << ans << "\n";
    }
    return 0;
}

7.3 代码要点

要点 说明
mulmod + __int128 防 \(10^9\times10^9\) 溢出(实测 P4195 在 \(p\sim10^9\) 下必要)
custom_hash 防洛谷对 unordered_map 默认哈希的攻击数据
baby[cur]=j 覆盖写 保留最大 \(j\),使 \(x=im-j\) 最小
while(m*m<p)m++ 整数法求 \(\lceil\sqrt p\rceil\),避免浮点误差
循环开头 A%P==B%P 统一检查 \(x=k\)(含 \(x=0\)),防漏解
B 不取模 \(B=b/\prod d_i\le 10^9\),可直接整数除法
A 取模 \(A\) 可能极大,只维护 \(A\bmod P\)

八、手算示例

示例 1:约简中直接命中 ------ \(2^x\equiv 8\pmod{16}\)

初值 \(A=1,B=8,P=16,k=0\)。

检查 \(x=k\) \(d=\gcd(2,P)\) \(d\mid B\)? 更新后 \((A,B,P,k)\)
1 \(1\ne8\) 2 \((1,4,8,1)\)
2 \(1\ne4\) 2 \((1,2,4,2)\)
3 \(1\ne2\) 2 \((1,1,2,3)\)
4 \(1\equiv1\) --- --- 返回 \(k=3\)

验证:\(2^3=8\equiv8\pmod{16}\)。✓

示例 2:约简后调用 BSGS ------ \(4^x\equiv 2\pmod{14}\)

初值 \(A=1,B=2,P=14,k=0\)。

约简:第 1 轮 \(d=\gcd(4,14)=2\),\(2\mid2\),更新 \(A=1\cdot(4/2)\bmod7=2,\;B=1,\;P=7,\;k=1\);第 2 轮 \(d=\gcd(4,7)=1\) 跳出。

转入 BSGS:解 \(4^{x-1}\equiv 1\cdot2^{-1}\equiv4\pmod7\)。\(m=\lceil\sqrt7\rceil=3\)。

  • Baby:\(4\cdot4^j\bmod7\) 为 \(4,2,1\)(\(j=0,1,2\))。
  • \(a^m=4^3\equiv1\pmod7\)。Giant:\(i=1\) 时 \((a^m)^1=1\),查表命中 \(j=2\),\(t=1\cdot3-2=1\)。

\(x=k+t=1+1=\boxed{2}\)。验证:\(4^2=16\equiv2\pmod{14}\)。✓

示例 3:无解判定 ------ \(6^x\equiv 2\pmod{18}\)

初值 \(A=1,B=2,P=18,k=0\)。第 1 轮 \(d=\gcd(6,18)=6\),\(B\bmod d=2\bmod6=2\ne0\) → 无解

验证:\(6^0=1,\;6^1=6,\;6^2\equiv0,\dots\) 均不为 \(2\)。✓

示例 4(P4195 风格):\(5^x\equiv 33\pmod{58}\)

\(\gcd(5,58)=1\),直接进入 BSGS。\(\text{target}=33\)。\(m=8\)。

  • Baby(\(33\cdot5^j\bmod58\)):\(33,49,13,7,35,1,5,25\)。
  • \(5^8\bmod58=53\)。Giant:\(i=1\to53\)(未中);\(i=2\to53^2\bmod58=25\)(命中 \(j=7\))。

\(t=2\cdot8-7=9\),\(x=0+9=\boxed{9}\)。验证:\(5^9\bmod58=33\)。✓

(另:\(2^x\equiv3\pmod4\to\) 第 1 轮 \(d=2\),\(3\bmod2=1\ne0\) → No Solution;\(2^x\equiv8\pmod{12}\to\) 两步消因子后 BSGS 得 \(x=3\)。)


九、复杂度分析

阶段 时间 说明
消因子循环 \(O(\log p)\) 每步 \(P\) 至少减半,最多 \(\log_2 p\) 次,每步一次 \(\gcd\)
BSGS \(O(\sqrt p)\) Baby+Giant 各 \(O(\sqrt p)\)
总计 \(O(\sqrt p)\) \(\log p\) 被 \(\sqrt p\) 吸收

空间复杂度 \(O(\sqrt p)\)(哈希表)。P4195 约束 \(\sum\sqrt p\le5\times10^6\),完全可行。


十、边界情况与易错点

  1. \(x=0\) 必须检查 :\(a^0=1\),当且仅当 \(b\equiv1\pmod p\) 是解。循环开头 A%P==B%P 在 \(k=0\) 时即检查 \(1\equiv b\pmod p\),天然覆盖。
  2. \(p=1\) :模 1 下恒为 0,\(a^0=1\equiv0\equiv b\),答案恒 0(函数开头特判)。
  3. \(a\bmod p=0\)(即 \(p\mid a\)) :\(\gcd(a,p)=p\),消去后 \(a/d=0\);若 \(p\mid b\) 则最终 \(A\equiv B\pmod1\) 返回 \(k=1\),否则无解。对应 \(0^x\equiv b\) 仅当 \(b\equiv0\) 且 \(x\ge1\)。
  4. \(b\bmod p=0\) :需 \(p\mid a^x\),算法持续消去 \(\gcd(a,P)\) 直至 \(P=1\)(返回 \(k\))或某步 \(d\nmid B\)(无解)。
  5. \(b>p\) :先取 \(b\bmod p\),同余等价,不影响正确性。
  6. \(A\) 更新顺序 :必须先 P/=d 再用 \(P\) 对 \(A\) 取模(\(A\leftarrow A\cdot(a/d)\bmod(P/d)\)),否则不变量失效。
  7. 浮点误差(ll)ceil(sqrt((double)p)) 在 \(p\sim10^9\) 可能偏小 1,改用整数法 while(m*m<p)m++
  8. Hack 数据重点 :① \(b\bmod p=1\) 且 \(\gcd(a,p)\ne1\)(答案应为 0);② 消因子中途 \(x=k\) 命中(如 \(a=6,p=12,b=6\to x=1\));③ \(a\bmod p=0\) 且 \(b\bmod p=0\);④ \(b>p\)。

最常见错误:忘记检查 \(x=k\)

cpp 复制代码
// 错误:循环中漏掉 A%P==B%P 检查
while (true) {
    d = gcd(a, P);
    if (d == 1) break;
    if (B % d != 0) return -1;
    // 缺少检查!
    A = A * (a/d) % (P/d); B /= d; P /= d; k++;
}

Hack :\(a=6,\,p=12,\,b=6\)。正确答案 \(x=1\)(\(6^1\equiv6\))。漏检时会在第 2 步因 \(B\bmod2\ne0\) 误判无解。修复 :每轮开头检查 A%P==B%P


十一、总结

项目 内容
问题 \(a^x\equiv b\pmod p\),不要求 \(\gcd(a,p)=1\)
核心思想 逐步消去 \(\gcd(a,P)\),缩减模数至与 \(a\) 互素,再调 BSGS
不变量 \(A\cdot a^{x-k}\equiv B\pmod P\)(初值 \(A=1,B=b,P=p,k=0\))
循环五步 查 \(x=k\) → \(d=1\) 跳出 → \(d\nmid B\) 无解 → 同除 \(d\) 更新 → 继续
关键证明 \(\gcd(a,P)=1\Rightarrow\gcd(A,P)=1\)(保证 \(A\) 可逆);与原问题等价
时间复杂度 \(O(\sqrt p)\)
空间复杂度 \(O(\sqrt p)\)
易错点 查 \(x=k\);A 用新 \(P\) 取模;浮点精度;哈希攻击;溢出

与 BSGS 的关系 :exBSGS 是 BSGS 的严格扩展。当 \(\gcd(a,p)=1\) 时,循环首轮即 \(d=1\) 跳出,直接调 BSGS,行为与普通 BSGS 完全一致。
部分内容由 AI 润色

相关推荐
不会就选b1 小时前
算法日常・每日刷题--<贪心+大根堆>2
数据结构·算法
码艺-Alimjan1 小时前
维吾尔语数字转文本
java·javascript·python·算法·go
AI服务老曹1 小时前
算法任务配置完整流程:明厨亮灶项目从0到1怎么做 | AI视频分析算法实践
人工智能·算法·音视频
hahaha60161 小时前
HLS高层次综合设计--axi之burst纠偏
开发语言·算法·fpga开发·c#
hans汉斯1 小时前
【计算机科学与应用】基于联合熵驱动改进麻雀搜索优化VMD的DAS信号去噪方法
深度学习·算法·yolo·软件工程·汉斯出版社
词却1 小时前
机器学习入门:手写数字识别与算法对比
人工智能·算法·机器学习
张欣-男2 小时前
3分钟理解线性代数
线性代数·算法
白狐_7982 小时前
408 数据结构|外部排序优化:怎么减少时间开销
数据结构·算法
小白说大模型2 小时前
Hermes 全配置指南:从裸版到 AI Agent 天花板
大数据·人工智能·学习·算法·机器学习·数据挖掘