【算法笔记】Baby Step Giant Step(BSGS)及其扩展

  • 本文总计约 8000 字,阅读大约需要 30 分钟
  • 警告!警告!警告!本文有大量的 LATEX 公式渲染,可能会导致加载异常缓慢!

大手牵小手,大步小步走。

前言

BSGS,虽然在洛谷上是蓝题的水平,但实际上的难度远远不及蓝题。

然而,BSGS 在解决离散对数问题上非常有用,在历年省选和 NOI 中,考察 BSGS 的问题都非常的多。所以它的重要性又不止是蓝题。

更进一步地,BSGS 背后的思想— —分块思想,也是非常重要的思想了,所以,学习 BSGS,是一件很有用的事。

题目引入

给定 a,b,m,是否存在一个正整数 x,使得 axb(modm)?若存在,求出 b 的最小值。

例如:a=2b=3p=5,那么显然,当 x=3 时,有 2383(mod5)

再例如:a=2b=3p=10 时,不存在这样的 x

Subtask 1a,b109p103
Subtask 2a,b109p109,保证 ap
Subtask 2a,b109p109

上面的这个问题,就是非常经典的离散对数问题(Discrete Logarithm),这个问题在密码学中起了很大的贡献。

暴力算法

依照惯例,我们应该问一下:暴力怎么做?

当然,我们可以暴力枚举 x,计算 axmodp 的值。根据 Euler 定理,这个最小的 x 应该是 O(φ(p)) 级别的。更具体地,应该有 x2φ(p)<2p

这也就是说,我们可以通过枚举 12p 中的每一个 x,计算 axb(modp) 是否成立,这个过程的时间复杂度是 O(p) 的。

代码很简洁,就这么写:

#include <iostream> 
using namespace std;

typedef long long ll;

ll a, p, b;
ll ans = 1; 

int main(void) {
	scanf("%lld%lld%lld", &p, &a, &b);  //判断是否存在 x, 使得 a^x mod p = b 
	b %= p;

	for(int i = 0; i <= (p << 1); ++i) {  //暴力枚举所有的 x 
		if(ans == b) {
			printf("%d", i);  //判断是否满足 
			return 0; 
		} 
		ans = (ans * a) % p; 
	} 

	printf("No Solution");  //如果枚举完了都没有发现符合条件的 x,那就是无解 
	return 0; 
} 
//by CaO 

BSGS

尽管 O(p) 的枚举可以解决这个问题,但它的性能显然不够好

如果把 p 开到 109 级别,那么上面的这个代码就会 TLE 了。所以,我们就要想一个更加优雅的暴力来枚举所有的 x

『什么啊,所以还是要暴力枚举吗?』

确实如此。但是,如果我们改变了枚举的方法,那么它的速度就会有质的飞跃。

顺带提一下,在 OI 中,被称为『优雅的暴力』的算法是什么?

『唔……分块吗?』

对,所以,作为另外一种『优雅的暴力』,BSGS 也是基于分块的思想建立起来的。

BSGS 的思路

我们不妨把待求的 x 分解一下,给定一个 A,那么 x 可以写成 x=mAn 的形式。

原来的式子,就可以变形为:

amAnb(modp).

两边同时乘以 an,就有:

amAban(modp).

当然,这里的 n 应该不大于 AmpA

我们可以先暴力枚举每一个 n,并把 banmodp 的值用 Hash 表存储起来;然后,再枚举每一个 m,判断 amAmodp 在 Hash 表中有没有对应的元素。

那么,枚举 n 的复杂度为 Θ(A),枚举 m 的复杂度为 O(pA)

故总复杂度为 O(A+nA)。由均值不等式,我们取 A=n,这个时候时间复杂度取到最优 O(n) 了!

看,我们在枚举右面的 ban 的时候,是细细地按照 a 的指数,一小步一小步枚举的;而枚举左面的 amA 的时候,则是以 aA 的指数,大步大步走着枚举的。

所以,这个算法就被形象地称为大步小步算法(Baby Step, Giant Step,简称 BSGS)。

当然,也有人戏称为拔山盖世算法,或者北上广深算法 QwQ,但那都是后话了

代码

这个代码并不好写,有的时候,还要加一些奇奇怪怪的特判,比如[SDOI2011]计算器,这道题如果不加特判的话,只能拿到 7090 pts。这就非常考察选手对算法的理解程度了。

#include <algorithm>
#include <iostream>
#include <cmath>
#include <map>
using namespace std;

typedef long long ll;

map<ll, ll> mp;
ll a, b, p, ans;

ll qpow(ll a, ll b, ll p) {  //快速幂
    ll ans = 1;
    while(b) {
        if(b & 1) ans = (ans * a) % p;
        a = (a * a) % p; b >>= 1;
    }
    return ans;
}

ll bsgs(ll a, ll b, ll p) {  //BSGS 主体
    if(a % p == b % p) return 1;  //这个时候 a 和 b 模 p 同余,那么 x = 1 应该是最小的整数解
    if(a % p == 0 && b) return -1;  //如果 a = 0 但是 b 不为 0,那么很显然无解
    
    ll unit = (ll)ceil(sqrt(p)), tmp = qpow(a, unit, p);

    for(int i = 0; i <= unit; ++i)
        mp[b] = i, b = (b * a) % p;  //Hash 记录每个小步的情况

    b = 1;

    for(int i = 1; i <= unit; ++i) {  //枚举每个大步
        b = (b * tmp) % p;
        if(mp[b]) return i * unit - mp[b];
    }

    return -1;  //如果没有找到满足题意的,返回无解
}

int main(void) {
    scanf("%lld%lld%lld", &p, &a, &b);

    ll ans = BSGS(a, b, p);

    if(ans == -1) printf("No Solution");
    else printf("%lld", ans);
    return 0;
}
//by CaO

当然,如果用手写的 Hash,当然是 O(n) 的。但,好多时候,手写 Hash 非常麻烦,而且容易出错,所以用 C++ 中的 std::map,因为 std::map 本质上是一棵红黑树,时间复杂度会比 O(1) 的 Hash 要多个 log,所以用 std::map 实现的 BSGS,时间复杂度实际上是 O(nlogn)

BSGS 的拓展

然而,上面的 BSGS,是基于 ap 时的情况给出的,因为 ap 不互质的时候,Euler 定理不一定成立。所以这个时候,传统的 BSGS 就会失灵。

我们就需要想一下,如何把 BSGS 扩展到可以解决 ap 不互质的情况下呢?

我们把题目所给的这个式子搬出来:

axb(modp).

我们不妨设 gcd(a,p)=g,那么一定有:

agax1bg(modpg).

当然,b 不一定被 g 整除,这个时候同余方程就是无解的。

但是即使 b 能被 g 整除,我们也不能保证此时一定有 apg,怎么办?

那我们再对这个方程除以 gcd(a,pg),不断地重复这个操作,直到 apg 即可,即:

akgiaxkbkgi(modpkgi).

这个时候,两边同时乘以 (akgi)1,得到:

axk(akgi)1bkgi(modpkgi).

这样一个方程,我们就可以用传统的 BSGS 解决了!

像这样,先将底数和模数通过不断除以最大公约数的方式使它们互质,然后用 BSGS 解决离散对数的算法,叫做扩展 BSGS,又称 exBSGS

可以证明,这个算法的时间复杂度为 O(p+log2p)=O(p)

代码

代码如下:

#include <algorithm>
#include <iostream>
#include <cmath>
#include <map>
using namespace std;

typedef long long ll;

map<ll, ll> mp;
ll a, b, p, ans;

ll exgcd(ll a, ll b, ll& x, ll& y) {  //求逆元和 gcd 都可以使用扩欧,非常方便 QwQ
	if(!b) {
		x = 1, y = 0;
		return a;
	}

	ll ans = exgcd(b, a % b, y, x);
	y -= a / b * x;
	return ans;
}

ll inv(ll a, ll p) {  //求 a 模 p 的逆元
	ll x, y;
	exgcd(a, p, x, y);
	return (x % p + p) % p;
}

ll qpow(ll a, ll b, ll p) {  //快速幂
	ll ans = 1;
	while(b) {
		if(b & 1) ans = (ans * a) % p;
		a = (a * a) % p; b >>= 1;
	}
	return ans;
}

ll BSGS(ll a, ll b, ll p) {  //BSGS 主体,不解释了
	ll unit = (ll)ceil(sqrt(p)), tmp = qpow(a, unit, p);

	for(int i = 0; i <= unit; ++i)
		mp[b] = i, b = (b * a) % p;

	b = 1;

	for(int i = 1; i <= unit; ++i) {
		b = (b * tmp) % p;
		if(mp[b]) return i * unit - mp[b];
	}

	return -1;
}

ll exBSGS(ll a, ll b, ll p) {
	ll x, y, g = exgcd(a, p, x, y), k = 0, tmp = 1;

	while(g != 1) {  //当 gcd(a, p) 不为 1 时,就不断除以 gcd(a, p) 直到 a 与 p 互质
		if(b % g) return -1;  //b 不能被 gcd(a, p) 整除,当然不互质

		++k, b /= g, p /= g, tmp = tmp * (a / g) % p;

		if(tmp == b) return k;

		g = exgcd(a, p, x, y);
	}
	//用传统 BSGS 来解决问题
	ll ans = BSGS(a, b * inv(tmp, p) % p, p);
	if(ans == -1) return -1;
	return ans + k;
}

int main(void) {
	scanf("%lld%lld%lld", &p, &a, &b);

	ll ans = exBSGS(a, b, p);

	if(ans == -1) printf("no solution");
	else printf("%lld", ans);
	return 0;
}
//by CaO

例题

本题目列表会持续更新

posted @   CaO氧化钙  阅读(1915)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示