数论:从提高组到提高组
说明
最近可爱的 MGJ 连上了七天的数论课,然后她写了一篇提高组数论的合集。
由于篇幅原因,大部分例题不放代码。
此文章部分参考他人文章或借他人文章进行优化,链接如下:
- https://www.luogu.com.cn/problem/solution/P5656;
- https://www.luogu.com.cn/article/fcv6pwn2;
- https://www.luogu.com.cn/article/n6if1g3y。
更新日志:
- 2026.3.4 22:35:更新扩展欧几里得(Exgcd);
- 2026.3.5:更新乘法逆元;
- 2026.3.7:更新欧拉函数;
- 2026.3.8-3.9:更新中国剩余定理(CRT);
- 2026.3.9-3.10:更新欧拉定理;
- 2026.3.11:更新小步大步算法(BSGS);
- 2026.3.11 21:36:完善所有内容,整改错别字。
本文耗时
扩展欧几里得(Exgcd)
Part 0:前置知识
- 基础数学(四则运算,符号定义等);
- 欧几里得定理:
\gcd(a, b) = \gcd(b,\ a \bmod b) ; - 裴蜀定理。
Part 1:扩展欧几里得的定义
扩展欧几里得是一个算法,一般解决如下核心问题:
给定两个整数
这个方程叫做裴蜀方程(Bézout's identity)。
它在信息学竞赛中有着广泛应用,如求解一次不定方程或同余方程。
Part 2:裴蜀方程的求解
我们使用递归推导。
我们对
将
与原式比较,得到一组特解:
于是,我们找到了上层
最终当
::::warning[注意]
信息学大忌:在
下面给出两种模板代码,第一种好理解但代码较长(也长不了多少),新手建议使用第一种:
::::success[Code 1]
ll Exgcd(ll a, ll b, ll &x, ll &y) {
if (!b) {
x = 1, y = 0;
return a;
}
ll d = Exgcd(b, a % b, x, y), tmp = x;
x = y, y = tmp - (a / b) * y;
return d;
}
::::
::::success[Code 2]
ll Exgcd(ll a, ll b, ll &x, ll &y) {
if (!b) {
x = 1, y = 0;
return a;
}
ll d = Exgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
::::
Part 3:扩展欧几里得的应用
3.1:解不定方程
常见问题:求不定方程
首先,先使用裴蜀定理判断是否有解:由裴蜀定理得
然后,用 Exgcd 求出
两边同乘
与原式比较,可以得到一组特解:
有了特解,那么就可以推导通解了。
显然有:
令
则:
展开,得:
又
通过各种方法,解得:
于是,我们得到通解形式:
其中
我们还可以推导出,如果要求最小非负整数解,可以提前将系数
3.2:解同余方程
常见问题:解形如
考虑将此方程尽可能写成
于是就变成了不定方程的形式。
Part 4:例题
P1082 [NOIP 2012 提高组] 同余方程 / P2613【模板】有理数取余
两题均为套模板题。
讲解几个需要注意的点:
- 在 P1082 中,为了避免
x_0 为负数,我们需要先x \to x \bmod b ,再将x \to x + b 。但这可能并不是最小整数解,所以我们需要调整为x' = (x_0 \bmod b + b) \bmod b ; - 在 P2613 中,非负整数
a, b 的范围达到10 ^{10001} ,所以需要结合快读一边读入一边取模; - 在 P2613 中,需要判断无解情况。
P1516 青蛙的约会
如果他们相遇,他们初始的位置坐标之差和跳的距离应该在模
令
套模板即可,注意
P5656 【模板】二元一次不定方程 (exgcd)
模板简单,模板题不一定简单。因为此题细节很多,需要一一处理。
下面所有变量源于 Part 3 的应用 1。
那么我们先求最小值。首先,肯定的是用 Exgcd 求出一组特解
然后就可以求最大值了。最大值怎么求?很显然,对于
你以为这就结束了?我们还没有处理没有正整数解的情况。当我们限制
::::success[Code]
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 2e5 + 10;
ll a, b, c, d, x, y, dx, dy, m, n, xmn, xmx, ymn, ymx;
ll Exgcd(ll a, ll b, ll &x, ll &y) {
if (!b) {
x = 1, y = 0;
return a;
}
ll res = Exgcd(b, a % b, y, x);
y -= a / b * x;
return res;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t;
for (cin >> t; t; -- t) {
cin >> a >> b >> c;
d = Exgcd(a, b, x, y);
if (c % d) {
cout << "-1\n";
continue;
}
dx = b / d, dy = a / d;
x *= c / d, y *= c / d;
m = ceil(1.0 * (1 - x) / dx), n = floor(1.0 * (y - 1) / dy);
if (m > n) {
cout << x + m * dx << ' ' << y - n * dy << '\n';
} else {
xmn = (x % dx + dx) % dx;
if (!xmn) {
xmn = dx;
}
ymn = (y % dy + dy) % dy;
if (!ymn) {
ymn = dy;
}
xmx = (c - ymn * b) / a, ymx = (c - xmn * a) / b;
cout << n - m + 1 << ' ' << xmn << ' ' << ymn << ' ' << xmx << ' ' << ymx << '\n';
}
}
return 0;
}
::::
乘法逆元
Part 0:前置知识
- 基础数学(四则运算,符号定义等);
- 费马小定理。
Part 1:乘法逆元的定义
1.1:为什么需要乘法逆元?
我们先来探讨一下,为什么需要乘法逆元?
因为,对于加减乘运算,我们都可以通过四则运算和模运算进行取模。但是,对于除法:
1.2:乘法逆元的定义
对于整数
a 和模数m (m > 1 ),如果存在整数x 满足:ax \equiv 1 \pmod p 则称
x 是a 在模m 意义下的乘法逆元,记作a^{-1} \equiv x \pmod p 。
::::warning[注意]
这里的
1.3:逆元存在的条件
定理:
证明:
- 必要性:如果
\gcd(a, p) > 1 ,假设存在逆元x ,那么ax = 1 + kp 。由于\gcd(a, p) \mid a 且\gcd(a, p) \mid p ,所以\gcd(a, b) \mid 1 ,矛盾。 - 充分性:如果
\gcd(a, p) = 1 ,根据 Exgcd,存在x, y \in \mathbb{Z} 使得ax + py = 1 ,于是ax \equiv 1 \pmod p 。
Part 2:乘法逆元的求法
2.1:Exgcd 求逆元
- 原理:将同余方程转化为不定方程;
- 优点:有判断无解的步骤,当
p 不是质数时也可使用; - 时间复杂度:单个
\mathcal{O}(\log x) 。
我们将同余方程
于是就可以使用 Exgcd 求解
::::success[Code]
int Exgcd(int a, int b, int &x, int &y) {
if (!b) {
x = 1, y = 0;
return a;
}
ll res = Exgcd(b, a % b, y, x);
y -= a / b * x;
return res;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> a >> b;
Exgcd(a, b, x, y);
cout << (x % b + b) % b << '\n';
return 0;
}
::::
2.2 费马小定理求逆元
- 优点:代码简单;
- 缺点:
p 必须为质数; - 时间复杂度:
\mathcal{O}(\log p) 。
什么是费马小定理:
如果
p 是质数,且\gcd(a, p) = 1 ,那么a^{p - 1} \equiv 1 \pmod p
现在我们开始求逆元:
由
由于
于是,我们可以使用快速幂计算
::::success[Code]
ll Qpow(ll a, ll b, ll p) {
ll ans = 1;
for (; b; b >>= 1) {
if (b & 1) {
ans = ans * a % p;
}
a = a * a % p;
}
return ans;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> p;
cout << Qpow(n, p - 2) << '\n';
return 0;
}
::::
2.3 线性递推求逆元
- 优点:可以一次性求
n 以内所有正整数模p 的逆元; - 缺点:需满足
p 为质数且n < p ; - 时间复杂度:
\mathcal{O}(n) 。
令
边界条件为:
证明:
令
在模
两边同乘
所以:
即:
带入
得证。
::::success[Code]
inv[0] = 0, inv[1] = 1;
for (int i = 2; i <= n; ++ i) {
inv[i] = (p - p / i) * inv[p % i] % p;
}
::::
2.4 阶乘逆元的线性求法
- 优点:可以一次性求
n 以内所有正整数的阶乘模p 的逆元; - 条件:
p 为质数; - 时间复杂度:
\mathcal{O}(n) 。
这种方法在组合数中用的比较多。
- 先递推计算阶乘:
\text{fac}_i = (\text{fac}_{i - 1} \cdot i) \bmod p (边界条件:\text{fac}_1 = 1 ); - 用 Exgcd 或费马小定理求出
n! 的逆元\text{invfac}_n ; - 递推求其他阶乘逆元:
\text{invfac}_i = ((i + 1) \cdot \text{invfac}_i) \bmod p 。
证明:
因为
::::success[Code]
ll Qpow(ll a, ll b) {
ll ans = 1;
for (; b; b >>= 1) {
if (b & 1) {
ans = ans * a % kMod;
}
a = a * a % kMod;
}
return ans;
}
void Init() {
fac[0] = fac[1] = 1;
for (int i = 2; i <= n; ++ i) {
fac[i] = fac[i - 1] * i % kMod;
}
inv[n] = Qpow(fac[n], kMod - 2);
for (int i = n - 1; i; -- i) {
inv[i] = inv[i + 1] * (i + 1) % kMod;
}
}
::::
Part 3:例题
其实单独考乘法逆元的题目很少,主要结合组合数学。
P1082 [NOIP 2012 提高组] 同余方程
这不是逆元板子题吗?
由于
P3811 【模板】模意义下的乘法逆元
考虑使用费马小定理,但提交之后发现 TLE 了。
分析一下,费马小定理求逆元单次是
所以必须使用线性递推求逆元,时间复杂度
P2265 路边的水沟
简单分析可得,右下角的闸门到左上角的闸门的总共有
而我们必须向上走
答案就是在
使用阶乘逆元的线性求法即可。
P5431 【模板】模意义下的乘法逆元 2
由于:
令
接下来求
因为:
所以:
别忘了,原式还有一个
本题需要配合快读使用。
::::success[Code]
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 5e6 + 10;
ll n, p, k, a[kMaxN], mul[kMaxN], inv[kMaxN], ans;
ll R() {
ll x = 0, f = 1;
char ch = getchar();
while (!isdigit(ch)) {
if (ch == '-') {
f = -1;
}
ch = getchar();
}
while (isdigit(ch)) {
x = x * 10 + (ch ^ 48);
ch = getchar();
}
return x * f;
}
ll Qpow(ll a, ll b) {
ll ans = 1;
for (; b; b >>= 1) {
if (b & 1) {
ans = ans * a % p;
}
a = a * a % p;
}
return ans;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
n = R(), p = R(), k = R(), mul[0] = 1;
for (int i = 1; i <= n; ++ i) {
a[i] = R();
mul[i] = mul[i - 1] * a[i] % p;
}
inv[n] = Qpow(mul[n], p - 2);
for (int i = n - 1; i; -- i) {
inv[i] = inv[i + 1] * a[i + 1] % p;
}
for (int i = n; i; -- i) {
ans = (ans + (inv[i] * mul[i - 1]) % p) % p * k % p;
}
cout << ans << '\n';
return 0;
}
::::
欧拉函数
Part 0:前置知识
- 基础数学(四则运算,符号定义等)。
- 最大公约数(gcd);
- 互质。
Part 1:欧拉函数的定义
在信息学中,我们常常要解决互质类问题或处理大指数取模运算,而欧拉定理和欧拉函数就是一个好帮手。
定义:欧拉函数
欧拉函数有一个核心公式,此公式用处较多。
若
展开写就是:
此公式可以使用下文的性质 1 证明。
Part 2:欧拉函数的性质
2.1:性质 1
对于任意质数
p ,有\varphi(p) = p - 1 ,对于p 的幂次p^k (k \geq 1 ),有\varphi(p^k) = p^k - p^{k - 1} = p^{k - 1}(p - 1) 。
证明:
在
在
2.2:性质 2
若
a \mid x ,则\varphi(ax) = a \cdot \varphi(x) 。
此性质证明计算量较大。
由
那么:
使用用欧拉函数公式:
对每一项:
因此
2.3:性质 3
欧拉函数是积性函数。
即,对于任意满足
\gcd(a, b) = 1 的整数a, b ,都有\varphi(ab) = \varphi(a)\varphi(b) 。特别地,当
n \bmod 2 = 1 时,\varphi(2n) = \varphi(n) 。
此性质证明需要用到作者的知识盲区,作者太弱所以就不写上去了。
Part 3:欧拉函数的计算
3.1:暴力计算欧拉函数
欧拉函数公式,枚举每一个质因子然后计算答案即可。
时间复杂度:单个
::::success[Code]
ll Phi(ll n) {
ll ans = n;
for (ll i = 2; i * i <= n; ++ i) {
if (!(n % i)) {
ans -= ans / i;
for (; !(n % i); n /= i);
}
}
if (n > 1) {
ans -= ans / n;
}
return ans;
}
::::
3.2 线性筛求解欧拉函数
我们借助线性筛来求解欧拉函数,这个求法主要依赖于欧拉函数的性质。先放代码,再讲原理。
::::success[Code]
const int kMaxN = 1e6 + 10;
ll p[kMaxN], phi[kMaxN], cnt;
bool vis[kMaxN];
void Init() {
vis[1] = phi[1] = 1;
for (ll i = 2; i < kMaxN; ++ i) {
if (!vis[i]) {
phi[i] = i - 1;
p[++ cnt] = i;
}
for (ll j = 1; j <= cnt && i * p[j] < kMaxN; ++ j) {
vis[i * p[j]] = 1;
phi[i * p[j]] = phi[i] * (p[j] - 1);
if (!(i % p[j])) {
phi[i * p[j]] = phi[i] * p[j];
break;
}
}
}
}
::::
令
考虑枚举
如果
然后,我们就开始筛除
时间复杂度
Part 4:例题
UVA11327 Enumerating Rational Numbers
解读一下代码,就可以发现对于每一个
于是尝试枚举
由于
CF1295D Same GCDs
首先对式子做一次辗转相除,得:
显然题目可以转化为求有多少
令
现在再次转换题目:求与
P1891 疯狂 LCM
遇到这种题,无脑暴力推式子:
分析上式,目前的目标就是求
假设你欧拉函数掌握的不是特别好,我们来找一下规律。假设
没错!对于
那么:
答案即为:
那么,我们先
::::success[Code]
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 1e6 + 10;
ll n, phi[kMaxN], p[kMaxN], f[kMaxN], cnt;
bool vis[kMaxN];
void Init() {
vis[1] = phi[1] = 1;
for (ll i = 2; i <= 1e6; ++ i) {
if (!vis[i]) {
phi[i] = i - 1;
p[++ cnt] = i;
}
for (ll j = 1; j <= cnt && i * p[j] <= 1e6; ++ j) {
vis[i * p[j]] = 1;
phi[i * p[j]] = phi[i] * (p[j] - 1);
if (!(i % p[j])) {
phi[i * p[j]] = phi[i] * p[j];
break;
}
}
}
for (ll i = 1; i <= 1e6; ++ i) {
for (ll j = i; j <= 1e6; j += i) {
f[j] += (phi[i] * i + 1) / 2;
}
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
Init();
int t;
for (cin >> t; t; -- t) {
cin >> n;
cout << n * f[n] << '\n';
}
return 0;
}
::::
中国剩余定理(CRT)
Part 0:前置知识
- 基础数学(四则运算,符号定义等)。
- 互质;
- 同余;
- 逆元;
- 裴蜀定理。
Part 1:中国剩余定理是什么?
中国剩余定理(Chinese Remainder Theorem,CRT),又称孙子定理,是数论中最具代表性的定理之一,最早记载于我国南北朝时期的数学著作《孙子算经》,其中经典的“物不知数”问题便是其雏形:“今有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二,问物几何?”
中国剩余定理一般用来求解以下一次线性同余方程组:
其中对于
Part 2:同余方程组的求法
我们考虑使用余数的可加性,那么题目转化为求出
那么
进一步转化,其实就是求这
由余数的可乘性,得
又
求出
::::success[Code]
ll Exgcd(ll a, ll b, ll &x, ll &y) {
if (!b) {
x = 1, y = 0;
return a;
}
ll d = Exgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
ll CRT() {
ll mul = 1, ans = 0;
for (int i = 1; i <= n; ++ i) {
mul *= b[i];
}
for (ll i = 1, k, x, y; i <= n; ++ i) {
k = mul / b[i];
Exgcd(k, b[i], x, y);
ans = (ans + k * b[i] * x % mul) % mul;
}
return (ans % mul + mul) % mul;
}
::::
Part 3:扩展中国剩余定理(ExCRT)
由于中国剩余定理的局限性太强(两两互质),所以我们需要扩展中国剩余定理。
在信息学中,CRT 与 ExCRT 复杂度一致。所以,只要学习扩展中国剩余定理就可以覆盖 CRT 的问题。
考虑如下方程组:
其中
首先,我们将式子等价转换。
对于
于是:
移项,得:
观察此式子,发现形态与线性不定方程相同,考虑使用扩展欧几里得(Exgcd)求解。
令
首先,根据裴蜀定理:若
设通过 Exgcd 得到:
两边同乘
得到一组特解:
那么,原方程组的通解为:
然后我们考虑使用数学归纳法。
设前
移项,得:
将同余方程转成一次不定方程,得:
若该方程无解,则说明前
::::success[Code]
ll Exgcd(ll a, ll b, ll &x, ll &y) {
if (!b) {
x = 1, y = 0;
return a;
}
ll res = Exgcd(b, a % b, y, x);
y -= a / b * x;
return res;
}
ll Qmul(ll a, ll b, ll p) {
ll ans = 0;
for (; b; b >>= 1) {
if (b & 1) {
ans = (ans + a) % p;
}
a = (a + a) % p;
}
return ans;
}
ll ExCRT() {
ans = b[1], lcm = m[1];
for (ll i = 2, d, x, y, k; i <= n; ++ i) {
b[i] = ((b[i] - ans) % m[i] + m[i]) % m[i];
d = Exgcd(lcm, m[i], x, y);
if (b[i] % d) {
ans = -1;
break;
}
k = Qmul(x, b[i] / d, m[i]);
ans += k * lcm, lcm = lcm / d * m[i];
ans = (ans % lcm + lcm) % lcm;
}
return ans;
}
::::
Part 4:例题
P1495 【模板】中国剩余定理(CRT)/ P4777 【模板】扩展中国剩余定理(EXCRT)
板子题,套模板即可。
注意 P1495 需要 __int128。
P3868 [TJOI2009] 猜数字
题目中的式子很难看,那我们就把它写成同余方程组:
因为
然后就是模板了。
CF687B Remainders Game
此题思维性较强。
我们由题目可以知道,Arya 可以从 Pari 手中得知
这时你会发现,这个方程组与 ExCRT 一摸一样!那么,我们结合 ExCRT,可以发现,通过任意的:
可以得到
因此,我们只需要求出
为什么呢?因为当
欧拉定理
Part 0:前置知识
- 基础数学(四则运算,符号定义等)。
- 互质。
Part 1:欧拉定理的内容
1.1:欧拉定理
若正整数
当
1.2:证明
接下来,我们证明欧拉定理。
但是,在证明之前,我们需要先了解一个关键的观察:
取
我们发现,得到的余数是
这不是巧合,这是一个普遍规律。
那么,接下来我们开始证明。
设所有小于等于
由欧拉函数,得
考虑将这些数分别乘上
接下来,我们需要证明三个性质。
性质 1:
因为
性质 2:对于
假设
因为
又
因为
与
性质 3:
将命题转化成证明
假设
矛盾,原命题得证。
三个性质证明完后,我们会发现一个关键结论。
现在我们有两个集合:
- 原始集合
S = \{r_1, r_2, \cdots, r_{\varphi(n)}\} ; - 乘积集合
S' = \{ar_1 \bmod n,\ ar_2 \bmod n,\ \cdots,\ ar_{\varphi(n)} \bmod n\} 。
因为乘积集合也具有以下三个性质:
-
- 对于
i \neq j ,有ar_i \not \equiv ar_j \pmod n ; -
这表明一个关键结论:
既然
将
所以有:
由于原始集合的乘积与
证毕。
Part 2:欧拉定理的应用
2.1:求乘法逆元
如果
证明:
::::warning[注意] 此处实际上比费马小定理求逆元的应用更广。 ::::
2.2:指数降幂
对于
证明:
令
Part 3:扩展欧拉定理
对于任意正整数
注意:不需要满足
扩展欧拉定理给出了降幂更加广泛的形式。
::::success[Code]
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 1e6 + 10;
ll a, b, m, kMod;
bool ok;
ll R() {
ll k = 0, f = 1;
char c = getchar();
while (c < '0' || c > '9') {
(c == '-') && (f = -1);
c = getchar();
}
while (c >= '0' && c <= '9') {
k = k * 10 + c - '0';
if (k >= kMod) {
ok = 1;
}
k %= kMod;
c = getchar();
}
return k * f;
}
ll Phi(ll n) {
ll ans = n;
for (ll i = 2; i * i <= n; ++ i) {
if (!(n % i)) {
ans -= ans / i;
for (; !(n % i); n /= i);
}
}
if (n > 1) {
ans -= ans / n;
}
return ans;
}
ll Qpow(ll b, ll p) {
ll ans = 1;
for (; p; p >>= 1) {
if (p & 1) {
ans = ans * b % m;
}
b = b * b % m;
}
return ans;
}
int main() {
cin >> a >> m;
kMod = Phi(m), b = R();
if (ok) {
b += kMod;
}
cout << Qpow(a, b) << '\n';
return 0;
}
::::
Part 4:例题
P5091 【模板】扩展欧拉定理
板子题,套模板即可。
P4139 上帝与集合的正确用法
上面的文字很多,不用在意。我们直接将这个数列转化成求:
也就是
考虑使用扩展欧拉定理。可以证明,指数满足
于是,考虑使用递归的形式,直到边界条件
P2350 [HAOI2012] 外星人
由于一般的题目不单独考欧拉定理,所以此题严格上归于“欧拉函数”。
题目的意思就是:给定
首先,人人皆知,只有
接下来,根据题目中给的提示,我们有:
观察到,式子中有一个
所以,我们只需要求出
- 当
i 是质数时,显然有递推式f_i = f_{i - 1} ; - 当
i 不是质数时,令i = p \cdot q 。那么根据乘积的性质,可以得到f_i = f_p + f_q 。
你以为这就结束了?错!如果当原数
所以,当原数
::::success[Code]
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 1e5 + 10;
ll m, f[kMaxN], p[kMaxN], q[kMaxN], cnt, ans;
bool isp[kMaxN];
void Init() {
f[1] = 1;
for (ll i = 2; i < kMaxN; ++ i) {
if (!isp[i]) {
p[++ cnt] = i, f[i] = f[i - 1];
}
for (ll j = 1; j <= cnt && p[j] * i < kMaxN; ++ j) {
isp[p[j] * i] = 1, f[p[j] * i] = f[p[j]] + f[i];
if (!(i % p[j])) {
break;
}
}
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t;
Init();
for (cin >> t; t; -- t) {
cin >> m, ans = 1;
for (int i = 1; i <= m; ++ i) {
cin >> p[i] >> q[i];
ans += f[p[i]] * q[i] + (p[i] % 2? 0 : -1);
}
cout << ans << '\n';
}
return 0;
}
::::
小步大步算法(BSGS)
Part 0:前置知识
- 基础数学(四则运算,符号定义等)。
- 模运算;
- 欧拉定理;
- 离散对数(可以不掌握)。
Part 1:小步大步算法
1.1:算法解决的问题
小步大步算法(Baby Step Giant Step)一般解决如下问题:
给定一个质数
或者报告无解。
1.2:算法思想
此算法的思想是根号类思想,结合 Meet in middle 的思想。
令
两边同乘
考虑枚举
再考虑枚举
此时,
总时间复杂度
::::success[Code]
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 1e5 + 10;
ll p, b, n;
ll Qpow(ll a, ll b, ll p) {
ll ans = 1;
for (; b; b >>= 1) {
if (b & 1) {
ans = ans * a % p;
}
a = a * a % p;
}
return ans;
}
ll BSGS(ll p, ll b, ll n) {
unordered_map<ll, ll> mp;
ll t = sqrt(p) + 1;
for (ll i = 1, mul = (n * b) % p; i <= t; ++ i, mul = (mul * b) % p) {
mp[mul % p] = i;
}
ll pw = Qpow(b, t, p);
for (ll i = 1, mul = pw; i <= t; ++ i, mul = (mul * pw) % p) {
if (mp[mul]) {
return t * i - mp[mul];
}
}
return -1;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> p >> b >> n;
ll ans = BSGS(p, b, n);
if (ans == -1) {
cout << "no solution\n";
} else {
cout << ans << '\n';
}
return 0;
}
::::
Part 2:扩展小步大步算法
在扩展小步大步算法(exBSGS)中,不要求
那么我们开始推导。
首先,我们尝试将同余方程写成不定方程:
根据裴蜀定理,当
再将不定方程写成同余方程的形式:
两边同乘
我们发现,这跟 BSGS 的形式完全相同。
所以,我们检查
::::success[Code]
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 2e5 + 10;
ll BSGS(ll a, ll p, ll b, ll pw) {
unordered_map<ll, ll> mp;
ll t = sqrt(p) + 1, mul = 1;
for (ll i = 1; i <= t; ++ i, mul = mul * a % p) {
mp[mul * b % p] = i;
}
for (ll i = 1, k = mul, mul = pw; i <= t + 1; ++ i, mul = mul * k % p) {
if (mp[mul] && t * (i - 1) - mp[mul] + 1 >= 0) {
return t * (i - 1) - mp[mul] + 1;
}
}
return -1;
}
ll exBSGS(ll a, ll p, ll b) {
a %= p, b %= p;
if (b == 1 || p == 1) {
return 0;
}
ll res = 0, mul = 1;
for (ll d; (d = __gcd(a, p)) != 1; ) {
if (b % d) {
return -1;
}
++ res, b /= d, p /= d;
mul = (mul * a / d) % p;
if (mul == b) {
return res;
}
}
ll ans = BSGS(a, p, b, mul);
return (ans == -1? ans : ans + res);
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
for (ll a, p, b, ans; ; ) {
cin >> a >> p >> b;
if (!a) {
return 0;
}
ans = exBSGS(a, p, b);
if (ans == -1) {
cout << "No Solution\n";
} else {
cout << ans << '\n';
}
}
return 0;
}
::::
Part 3:例题
P3846 【模板】BSGS / P4195 【模板】扩展 BSGS
模板题,套板子即可。
P4454 [CQOI2018] 破解D-H协议
首先,将题目中 2、3 步的式子写成同余方程形式:
这不就是 BSGS 的形式了?于是使用 BSGS 求出
P4884 多少个 1?
感觉这一题难度虚高。
首先,对
那么方程转化为:
两边同乘
移项,得:
于是就转化为标准 BSGS 形式了。