离散对数
定义
前置知识:阶与原根.
离散对数的定义方式和对数类似.取有原根的正整数模数 𝑚
,设其一个原根为 𝑔
. 对满足 (𝑎,𝑚) =1
的整数 𝑎
,我们知道必存在唯一的整数 0 ≤𝑘 <𝜑(𝑚)
使得
𝑔𝑘≡𝑎(mod𝑚)
我们称这个 𝑘
为以 𝑔
为底,模 𝑚
的离散对数,记作 𝑘 =ind𝑔𝑎
,在不引起混淆的情况下可记作 ind𝑎
.
显然 ind𝑔1 =0
,ind𝑔𝑔 =1
.
性质
离散对数的性质也和对数有诸多类似之处.
性质
设 𝑔
是模 𝑚
的原根,(𝑎,𝑚) =(𝑏,𝑚) =1
,则:
-
ind𝑔(𝑎𝑏) ≡ind𝑔𝑎 +ind𝑔𝑏(mod𝜑(𝑚))
进而 (∀𝑛 ∈𝐍), ind𝑔𝑎𝑛 ≡𝑛ind𝑔𝑎(mod𝜑(𝑚))
-
若 𝑔1
也是模 𝑚
的原根,则 ind𝑔𝑎 ≡ind𝑔1𝑎 ⋅ind𝑔𝑔1(mod𝜑(𝑚))
- 𝑎 ≡𝑏(mod𝑚) ⟺ ind𝑔𝑎 =ind𝑔𝑏

证明
- 𝑔ind𝑔(𝑎𝑏) ≡𝑎𝑏 ≡𝑔ind𝑔𝑎𝑔ind𝑔𝑏 ≡𝑔ind𝑔𝑎+ind𝑔𝑏(mod𝑚)

-
令 𝑥 =ind𝑔1𝑎
,则 𝑎 ≡𝑔𝑥1(mod𝑚)
. 又令 𝑦 =ind𝑔𝑔1
,则 𝑔1 ≡𝑔𝑦(mod𝑚)
.
故 𝑎 ≡𝑔𝑥𝑦(mod𝑚)
,即 ind𝑔𝑎 ≡𝑥𝑦 ≡ind𝑔1𝑎 ⋅ind𝑔𝑔1(mod𝜑(𝑚))
-
注意到
ind𝑔𝑎=ind𝑔𝑏⟺ind𝑔𝑎≡ind𝑔𝑏(mod𝜑(𝑚))⟺𝑔ind𝑔𝑎≡𝑔ind𝑔𝑏(mod𝑚)⟺𝑎≡𝑏(mod𝑚)
大步小步算法
目前离散对数问题仍不存在多项式时间经典算法(离散对数问题的输入规模是输入数据的位数).在密码学中,基于这一点人们设计了许多非对称加密算法,如 Ed25519.
在算法竞赛中,BSGS(baby-step giant-step,大步小步算法)常用于求解离散对数问题.形式化地说,对 𝑎,𝑏,𝑚 ∈𝐙+
,该算法可以在 𝑂(√𝑚)
的时间内求解
𝑎𝑥≡𝑏(mod𝑚)
其中 𝑎 ⟂𝑚
.方程的解 𝑥
满足 0 ≤𝑥 <𝑚
.(注意 𝑚
不一定是素数)
算法描述
令 𝑥 =𝐴⌈√𝑚⌉ −𝐵
,其中 0 ≤𝐴,𝐵 ≤⌈√𝑚⌉
,则有 𝑎𝐴⌈√𝑚⌉−𝐵 ≡𝑏(mod𝑚)
,稍加变换,则有 𝑎𝐴⌈√𝑚⌉ ≡𝑏𝑎𝐵(mod𝑚)
.
我们已知的是 𝑎,𝑏
,所以我们可以先算出等式右边的 𝑏𝑎𝐵
的所有取值,枚举 𝐵
,用 hash/map 存下来,然后逐一计算 𝑎𝐴⌈√𝑚⌉
,枚举 𝐴
,寻找是否有与之相等的 𝑏𝑎𝐵
,从而我们可以得到所有的 𝑥
,𝑥 =𝐴⌈√𝑚⌉ −𝐵
.
注意到 𝐴,𝐵
均小于 ⌈√𝑚⌉
,所以时间复杂度为 Θ(√𝑚)
,用 map 则多一个 log
.
为什么要求 𝑎
与 𝑚
互质
注意到我们求出的是 𝐴,𝐵
,我们需要保证从 𝑎𝐴⌈√𝑚⌉ ≡𝑏𝑎𝐵(mod𝑚)
可以推回 𝑎𝐴⌈√𝑚⌉−𝐵 ≡𝑏(mod𝑚)
,后式是前式左右两边除以 𝑎𝐵
得到,所以必须有 𝑎𝐵 ⟂𝑚
即 𝑎 ⟂𝑚
.
扩展 BSGS 算法
对 𝑎,𝑏,𝑚 ∈𝐙+
,求解
𝑎𝑥≡𝑏(mod𝑚)
其中 𝑎,𝑚
不一定互质.
当 (𝑎,𝑚) =1
时,在模 𝑚
意义下 𝑎
存在逆元,因此可以使用 BSGS 算法求解.于是我们想办法让他们变得互质.
具体地,设 𝑑1 =(𝑎,𝑚)
. 如果 𝑑1 ∤𝑏
,则原方程无解.否则我们把方程同时除以 𝑑1
,得到
𝑎𝑑1⋅𝑎𝑥−1≡𝑏𝑑1(mod𝑚𝑑1)
如果 𝑎
和 𝑚𝑑1
仍不互质就再除,设 𝑑2 =(𝑎,𝑚𝑑1)
. 如果 𝑑2 ∤𝑏𝑑1
,则方程无解;否则同时除以 𝑑2
得到
𝑎2𝑑1𝑑2⋅𝑎𝑥−2≡𝑏𝑑1𝑑2(mod𝑚𝑑1𝑑2)
同理,这样不停的判断下去,直到 𝑎 ⟂𝑚𝑑1𝑑2⋯𝑑𝑘
.
记 𝐷 =∏𝑘𝑖=1𝑑𝑖
,于是方程就变成了这样:
𝑎𝑘𝐷⋅𝑎𝑥−𝑘≡𝑏𝐷(mod𝑚𝐷)
由于 𝑎 ⟂𝑚𝐷
,于是推出 𝑎𝑘𝐷 ⟂𝑚𝐷
. 这样 𝑎𝑘𝐷
就有逆元了,于是把它丢到方程右边,这就是一个普通的 BSGS 问题了,于是求解 𝑥 −𝑘
后再加上 𝑘
就是原方程的解啦.
注意,不排除解小于等于 𝑘
的情况,所以在消因子之前做一下 Θ(𝑘)
枚举,直接验证 𝑎𝑖 ≡𝑏(mod𝑚)
,这样就能避免这种情况.
基于值域预处理的快速离散对数
前文的 BSGS 算法时间复杂度为单次 𝑂(√𝑚)
,在询问量级较大的时候效率较低.若每次求解的模数是一个固定的质数 𝑝
,我们就有一个基于值域预处理的快速算法.
我们已经知道 ind𝑔(𝑎𝑏) ≡ind𝑔𝑎 +ind𝑔𝑏(mod𝑝 −1)
,所以我们可以只对所有质数通过 BSGS 算法计算离散对数,合数的离散对数则可通过该式转化为若干已知的质数离散对数值之和.此时复杂度仍然不优,我们考虑只预处理一部分的离散对数,具体来说,我们预处理 1
到 𝐿 =⌊√𝑝⌋ +1
的离散对数.注意此时的 BSGS 块长 𝐵
不能取 𝑂(√𝐿)
,因为 BSGS 预处理(插入哈希表)部分的复杂度是 𝑂(𝐵)
,而查询一共需要 𝑂(𝜋(𝐿))
次,则总时间复杂度为 𝑂(𝐵+𝜋(𝐿)𝑝𝐵)
,此时取 𝐵 =𝑂(√𝜋(𝐿)𝑝)
才是最优.由 素数定理 𝜋(𝑛) ∼𝑛log𝑛
,则总的预处理时间复杂度可以平衡为 𝑂(𝑝3/4log1/2𝑝)
.
接下来是如何求答案.假设当前要求的是 ind𝑔𝑦
,若 𝑦 ≤𝐿
则直接返回,否则设 𝑝 =𝑣𝑦 +𝑟
,则 𝑣 =⌊𝑝𝑦⌋ <𝐿
,𝑟 =𝑝mod𝑦
,𝑦 =𝑝−𝑟𝑣
,从而
ind𝑔𝑦≡ind𝑔(𝑝−𝑟)−ind𝑔𝑣≡ind𝑔(−𝑟)−ind𝑔𝑣≡ind𝑔(𝑝−1)+ind𝑔𝑟−ind𝑔𝑣(mod𝑝−1).
注意到 ind𝑔(𝑝 −1) =(𝑝 −1)/2
,因此只需递归计算 𝑟
的离散对数即可.
我们还可以考虑 𝑦
的另一种表达方式,注意到 𝑝 =𝑣𝑦 +𝑟 =(𝑣 +1)𝑦 +𝑟 −𝑦
,则 𝑦 =𝑝−𝑟+𝑦𝑣+1
,从而
ind𝑔𝑦≡ind𝑔(𝑦−𝑟)−ind𝑔(𝑣+1)(mod𝑝−1).
我们有 𝑣 +1 ≤𝐿
,因此只需要递归计算 𝑦 −𝑟
的离散对数即可.
综合这两种计算方式,我们有 min{𝑟,𝑦 −𝑟} ≤𝑦2
,所以递归计算较小的一方即可达到 𝑂(log𝑝)
的查询复杂度.
至此我们得到了一个时间复杂度为 𝑂(𝑝3/4log1/2𝑝) −𝑂(log𝑝)
的算法.
Luogu11175【模板】基于值域预处理的快速离散对数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89 | #include <cmath>
#include <iostream>
#include <unordered_map>
using namespace std;
const int N = 1'000'000; // sqrt(P * sqrt(P) / ln(P))
unordered_map<int, int> M;
int Lg[N], p[N], t;
bool vis[N];
int P, g, B, sq, LP_1, inv;
int fadd(int x, int y, int P) {
x += y;
if (x >= P) x -= P;
return x;
}
int fsub(int x, int y, int P) {
x -= y;
if (x < 0) x += P;
return x;
}
int fmul(int x, int y, int P) { return 1ll * x * y % P; }
int qpow(int x, int y) {
int ans = 1;
while (y) {
if (y & 1) ans = fmul(ans, x, P);
x = fmul(x, x, P);
y >>= 1;
}
return ans;
}
int calc(int x) {
int s = x;
for (int i = 0; i <= P / B; ++i) {
if (M.find(s) != M.end()) return i * B + M[s];
s = fmul(s, inv, P);
}
return -1;
}
void init() {
int s = 1;
for (int i = 0; i < B; ++i) {
if (M.find(s) != M.end()) break;
M[s] = i, s = fmul(s, g, P);
}
inv = qpow(qpow(g, B), P - 2);
for (int i = 2; i <= sq; ++i) {
if (!vis[i]) {
p[++t] = i;
Lg[i] = calc(i);
}
for (int j = 1; j <= t && p[j] * i <= sq; ++j) {
vis[p[j] * i] = true;
Lg[p[j] * i] = fadd(Lg[p[j]], Lg[i], P - 1);
if (i % p[j] == 0) break;
}
}
}
int solve(int y) {
if (y <= sq) return Lg[y];
int v = P / y, r = P % y;
if (r < y - r) return fadd(fsub(solve(r), Lg[v], P - 1), LP_1, P - 1);
return fsub(solve(y - r), Lg[v + 1], P - 1);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> P >> g;
sq = sqrt(P) + 1;
B = sqrt(1ll * P * sqrt(P) / log(P));
init();
LP_1 = (P - 1) / 2; // g ^ LP_1 = P - 1 (mod P)
int T;
cin >> T;
while (T--) {
int x;
cin >> x;
cout << solve(x) << '\n';
}
return 0;
}
|
习题
本页面部分内容以及代码译自博文 Дискретное извлечение корня 与其英文翻译版 Discrete Root.其中俄文版版权协议为 Public Domain + Leave a Link;英文版版权协议为 CC-BY-SA 4.0.
参考资料
- Discrete logarithm - Wikipedia
- 潘承洞,潘承彪.初等数论.
- 冯克勤.初等数论及其应用.
本页面最近更新:2026/4/27 16:47:43,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面贡献者:Ir1d, StudyingFather, sshwy, Steaunk, Great-designer, H-J-Granger, Tiphereth-A, c-forrest, Enter-tainer, MegaOwIer, countercurrent-time, Henry-ZHR, Konano, ksyx, NachtgeistW, ouuan, stevebraveman, Xeonacid, Alpha1022, AngelKitty, CCXXXI, Chrogeek, ChungZH, cjsoft, diauweb, Early0v0, ezoixx130, FFjet, GavinZhengOI, GekkaSaori, Gesrua, HeRaNO, hly1204, hsfzLZH1, iamtwz, isdanni, Kelatte, kxccc, Lampese, LovelyBuggies, lychees, Makkiy, Marcythm, mgt, minghu6, P-Y-Y, Peanut-Tang, PotassiumWings, purple-vine, SamZhangQingChuan, Ssfz202601, SukkaW, Suyun514, weiyong1024, xyf007, YOYO-UIAT
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用