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)\)。
-
检查 \(x=k\) (每轮开头做,天然覆盖 \(x=0\)):若 \(A\equiv B\pmod P\),则 \(a^k\equiv b\pmod p\),直接返回 \(k\)。
-
\(d=1\),约简结束 :此时 \(\gcd(a,P)=1\),且可证 \(\gcd(A,P)=1\)(\(A\) 可逆),转入 BSGS。
-
\(d>1\) 且 \(d\nmid B\),无解 :对 \(x>k\),\(a^{x-k}\) 被 \(d\) 整除 \(\Rightarrow Aa^{x-k}\) 被 \(d\) 整除,但 \(B\) 不被 \(d\) 整除,矛盾。整个方程无解。
-
\(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\)。
-
回到第 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 最小性的保证
- \(x=0,1,\dots,k\) 在循环开头逐一被检查;
- \(x>k\) 的最小解由 BSGS 返回最小非负 \(t=x-k\) 保证,故 \(x=k+t\) 最小;
- 二者取并集得全局最小。
七、算法流程与 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\),完全可行。
十、边界情况与易错点
- \(x=0\) 必须检查 :\(a^0=1\),当且仅当 \(b\equiv1\pmod p\) 是解。循环开头
A%P==B%P在 \(k=0\) 时即检查 \(1\equiv b\pmod p\),天然覆盖。 - \(p=1\) :模 1 下恒为 0,\(a^0=1\equiv0\equiv b\),答案恒 0(函数开头特判)。
- \(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\)。
- \(b\bmod p=0\) :需 \(p\mid a^x\),算法持续消去 \(\gcd(a,P)\) 直至 \(P=1\)(返回 \(k\))或某步 \(d\nmid B\)(无解)。
- \(b>p\) :先取 \(b\bmod p\),同余等价,不影响正确性。
- \(A\) 更新顺序 :必须先
P/=d再用新 \(P\) 对 \(A\) 取模(\(A\leftarrow A\cdot(a/d)\bmod(P/d)\)),否则不变量失效。 - 浮点误差 :
(ll)ceil(sqrt((double)p))在 \(p\sim10^9\) 可能偏小 1,改用整数法while(m*m<p)m++。 - 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 润色