扩展BSGS
题目大意
给出 a,p,b,找到最小的非负整数 x,使得 a 的 x 次方在模 p 意义下等于 b。
思路
这个题就是扩展 BSGS 的模板题。
BSGS
那既然它是扩展,那我们来说说原版是怎么弄的。
BSGS 的意思是先小步后大步,它可以求出方程 Ax≡B(mod C) 且 gcd(A,C)=1 时的解。而且是用 √C 的时间。
那 O(C) 的大家都会把,就枚举 0∼C−1,有就输出,否则就是没有解。
但是为什么答案会出现在这里呢?
你会知道要么有很多种答案,要么没有答案。因为你 Ax 的取值在取模意义下只能是 0∼C−1,那一旦你有取值一样的,那跟下来也是一样。(因为你每次乘的都是同一个数)
那在最坏情况下,把范围内所有数都取完再重复,就是 O(C) 了。
然后 BSGS 就是一种神奇的算法,通过有点分块的感觉把它弄成了 O(√C)。
它是这样的,首先把 0∼C−1 分成连续的几组,让每组的个数都尽可能相等。
然后对于每组,我们询问在这个组中的数是否可以成为答案。
那你会问了,它还是 O(C) 的啊!
不急,我们看公式来优化。
我们来看对于块 i(1≤i≤n,n 是分成的块数),给每个点从左到右编号 0∼m−1(也就是每块 m 个,那会有 m=Cn),我们枚举每个编号 y,我们就是要看是否存在一个使得这个公式满足:
Aim−y≡B(mod C)
那我们开始化简:
首先,我们看到 Aim−y 上面有减的,而模数是质数,那我们就弄个逆元,然后再移到右边,就变成:
Aim≡AyB(mod C)
那你会发现,每次你枚举的 y 都是同样的 0∼m−1,那你可以把它预处理出来。然后你看左边,你会发现它只枚举了一个 i。
但是你是不可以每个都枚举另一边出现的每个可能,看是否存在相等的。(因为你数组存不了)
数组存不了,你自然会想到一个东西——哈希表。
那就把右边能取的每个值都放进哈希表里面,然后每次查询 Aim 对应的数是否在哈希表中有。
然后就是 n,m 的取值问题了,因为你枚举左边是 O(n),预处理右边是 O(m),那你要尽可能让它们相等,那就都取 √C。
那就是 BSGS 算法了,它的确定就是一定要 gcd(A,C)=1
扩展 BSGS
它就是来解决 BSGS 的缺点,使得它可以解决 gcd(A,C)≠1 的情况。
它主要的想法还是把它弄回互质的情况,然后再用 BSGS 解决。
我们观察式子:
Ax≡B(mod C)
那因为要化简,那我们弄成不定方程。
Ax+Cy=B
那我们就不停地把左边的化简到 gcd 为 1,但是 Ax 太大了,那我们考虑把它分成 x 个 A。
每次就除以 gcd(A,C),然后知道 gcd(Ax,Cy)=1,然后 B 也跟着除,如果不能整除了,就说明无解。
那我们假设一共除以了 w 次,第 i 次除的是 di。
那式子就变成了这个:
Aww∑i=1diAx−w+Cyw∑i=1di=Bw∑i=1di
这个时候,就互质了。
然后你就可以转回同余方程,按着 BSGS 算了。
扩展欧几里得求逆元
不会吧不会吧不会真有人不会吧。
哦我不会啊,那没事了。
其实就是 ax+my=1 解这个方程。(m 是模数)
很明显它可以解决模数和要逆元的数互质的情况。
为什么能这么求呢?
变成同于方程:
ax≡1(mod m)
那你要 x 是 a 的逆元,其实就是要满足这个式子。
(因为逆元定义就是取模意义下的倒数,那数和它的倒数相乘当然就是 1 了)
代码
#include<cmath>
#include<cstdio>
#include<cstring>
#define ll long long
#define hash_mo 999979
using namespace std;
struct node {
ll to, nxt, x;
}e[500001];
ll a, mo, b, n, now, t, KK;
ll hash[1000001], gcd, d, ans;
void csh() {
t = 0;
d = 1;
memset(hash, 0, sizeof(hash));
KK = 0;
}
void hash_push(ll x, ll id) {
int pl = x % hash_mo;
for (int i = hash[pl]; i; i = e[i].nxt)
if (e[i].to == x) {
e[i].x = id;
return ;
}
e[++KK] = (node){x, hash[pl], id}; hash[pl] = KK;
}
ll hash_ask(int q) {
int pl = q % hash_mo;
for (int i = hash[pl]; i; i = e[i].nxt)
if (e[i].to == q) {
return e[i].x;
}
return -1;
}
ll GCD(ll x, ll y) {
if (!y) return x;
return GCD(y, x % y);
}
ll exgcd(ll a, ll &x, ll b, ll &y) {
if (!b) {
x = 1;
y = 1;
return a;
}
ll re = exgcd(b, y, a % b, x);
y -= a / b * x;
return re;
}
ll ksm(ll x, ll y) {
ll re = 1;
while (y) {
if (y & 1) re = (re * x) % mo;
x = (x * x) % mo;
y >>= 1;
}
return re;
}
ll inv(ll x, ll mo) {
ll X, Y;
exgcd(x, X, mo, Y);
return (X % mo + mo) % mo;
}
ll get_hz() {
gcd = GCD(a, mo);
while (gcd != 1) {
if (b % gcd != 0) return 0;
t++;
mo /= gcd;
b /= gcd;
d = (d * (a / gcd) % mo) % mo;
gcd = GCD(a, mo);
}
return 1;
}
ll make_hash() {
n = sqrt(mo);
now = 1;
for (int i = 0; i < n; i++) {
if (now == b) return i + t;
hash_push(now * b % mo, i);
now = (now * a) % mo;
}
int ntimes = ksm(a, n);
now = ntimes;
for (int i = 1; i <= n; i++) {
int mz = hash_ask(now);
if (mz != -1) return i * n + t - mz;
now = (now * ntimes) % mo;
}
return -1;
}
int main() {
scanf("%lld %lld %lld", &a, &mo, &b);
while (a || mo || b) {
csh();
if (!get_hz()) printf("No Solution\n");
else {
b = (b * inv(d, mo)) % mo;
ans = make_hash();
if (ans == -1) printf("No Solution\n");
else printf("%lld\n", ans);
}
scanf("%lld %lld %lld", &a, &mo, &b);
}
return 0;
}
__EOF__
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现